Toast stack
Stack short results at the edge until you reach for them.
toastnotificationsonner
Preview
Toast stack preview
Live · keyboard ready
When to use
- App-wide notifications for async results, errors, and undo.
- Loading toasts that morph into success or error when work finishes.
- Toasts with an action button, like View or Undo.
When not to use
- Use toast for a single locally controlled confirmation.
- Use alert for persistent messages tied to a page.
- Use notification-center for a history people can come back to.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "toast-stack",
"type": "registry:ui",
"title": "Toast stack",
"description": "Stack short results at the edge until you reach for them.",
"categories": [
"feedback"
],
"dependencies": [
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/toast-stack/toast-stack.tsx",
"type": "registry:component",
"target": "@components/manicat/toast-stack/toast-stack.tsx"
},
{
"path": "registry/components/toast-stack/toast-stack.module.css",
"type": "registry:component",
"target": "@components/manicat/toast-stack/toast-stack.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/toast-stack",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/toast-stack",
"markdown": "https://uiarc.dev/components/toast-stack/markdown",
"tags": [
"toast",
"notification",
"sonner"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { useState } from "react";
import { ToastStack, ToastStackProvider, useToastStack } from "@/components/manicat/toast-stack/toast-stack";
import { Button } from "@/components/manicat/button/button";
function ToastStackDemoInner() {
const { toast, update, dismiss } = useToastStack();
const [count, setCount] = useState(0);
return (
<div className="flex flex-wrap items-center gap-3">
<Button
onClick={() => {
const id = toast({ title: "Publishing", description: "Uploading assets.", type: "loading" });
setTimeout(() => update(id, { title: "Published", description: "arc-library is live.", type: "success" }), 1400);
}}
>
Publish with a morphing toast
</Button>
<Button
variant="secondary"
onClick={() => {
setCount((value) => value + 1);
toast({ title: `Undo delete (${count + 1})`, type: "info", action: { label: "Undo", onClick: () => dismiss() } });
}}
>
Toast with an action
</Button>
<ToastStack contained position="bottom-center" />
</div>
);
}
export function ToastStackExample() {
return (
<ToastStackProvider>
<ToastStackDemoInner />
</ToastStackProvider>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Toasts from anywhere Mount the provider once, then call toast() from any handler; actions like Undo live inside the toast.
Toasts from anywhere
Mount the provider once, then call toast() from any handler; actions like Undo live inside the toast. preview
Live · keyboard ready
toasts-from-anywhere-mount-the-provider-once-then-call-toast-from-any-handler-actions-like-undo-live-inside-the-toast-.tsxtsx
"use client";
import { Button } from "@/components/manicat/button/button";
import { ToastStack, ToastStackProvider, useToastStack } from "@/components/manicat/toast-stack/toast-stack";
function ToastTriggers() {
const { toast, dismiss } = useToastStack();
return (
<div className="flex flex-wrap items-center gap-3">
<Button onClick={() => toast({ title: "Changes saved", type: "success" })}>Save</Button>
<Button
variant="secondary"
onClick={() => toast({ title: "Conversation deleted", type: "info", action: { label: "Undo", onClick: () => dismiss() } })}
>
Delete with undo
</Button>
<ToastStack contained position="bottom-center" />
</div>
);
}
export function ToastStackActionsExample() {
return (
<ToastStackProvider>
<ToastTriggers />
</ToastStackProvider>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| Alt+T | Moves focus to the front toast and fans out the stack. |
| Tab | Moves between toast actions and dismiss buttons. |
| Escape | Dismisses the focused toast. |
Accessibility
- The viewport is a labelled section with aria-live="polite" and aria-relevant="additions text", so new and updated toasts are announced.
- Each toast prefixes a screen-reader-only type label such as "Error:"; outgoing copy is aria-hidden while it fades.
- Hidden and leaving toasts are inert. When a focused toast closes, keyboard focus moves to the next toast or back to where it came from.
- Timers pause while the stack is hovered, focused, dragged, or the tab is hidden.
Motion
- New toasts rise from their own height below the edge; older ones tuck behind at 14px peeks and 0.05 scale steps, and fan out on a morph spring.
- Swiping right follows the finger and throws with velocity; left rubber-bands. Updates morph the icon, copy, height, and action width in place.
- Reduced motion jumps positions and uses short opacity fades.
Responsive behavior
- The viewport is min(22.5rem, 100% minus a 32px gutter) wide; below 30rem it centers at the bottom regardless of position.
- Below a 21rem toast width the action button drops under the copy instead of squeezing it.
- On touch, a tap fans out the stack, and a right swipe dismisses a toast.
Performance
- At most 12 toasts are kept by default and only 3 show; each has its own ResizeObserver for height.
- Timers pause while hovered, focused, dragged, or when the tab is hidden.
Notes for AI
- Default for app-wide notifications, async results, and undo. Use toast for a single locally controlled confirmation and alert for persistent inline messages.
- Render ToastStack once inside ToastStackProvider and call useToastStack anywhere below. Reuse an id or call update to morph loading into success or error.