Toast Stack
sonner-style collapsing toast stack — new toasts slide in from the edge, collapse into a pile of cards when not hovered, and expand into the full list while pausing the auto-dismiss countdown when the pointer enters the viewport.
npx shadcn@latest add https://webberui.com/r/toast-stack.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ToastStack />
Installation
npx shadcn@latest add https://webberui.com/r/toast-stack.jsonOr, once registries are configured in components.json, install it as @webberui/toast-stack.
Usage
Wrap ToastProvider once at the outermost level of your app (or around whatever scope needs notifications); any component inside can then send toasts through useToast():
import { ToastProvider, useToast } from "@/components/ui/toast-stack";
function App() {
return (
<ToastProvider position="bottom-right" duration={5000} max={5}>
<SaveButton />
</ToastProvider>
);
}
function SaveButton() {
const { toast } = useToast();
return (
<button
onClick={() =>
toast({
title: "Changes saved",
description: "Settings synced to all devices.",
variant: "success",
})
}
>
Save
</button>
);
}toast() returns the id of that toast; combined with dismiss(id) you can close it early once the flow completes:
const { toast, dismiss } = useToast();
const id = toast({ title: "Uploading…" });
// Close manually once done
dismiss(id);The viewport is built into ToastProvider and rendered into document.body via createPortal, so there is no separate viewport component to mount.
Props
ToastProvider
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | App content; useToast() must be called within this scope |
position | 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-right' | The corner the viewport docks to |
duration | number | 5000 | Milliseconds before auto-dismiss; set to 0 or below, or Infinity, to disable auto-dismiss |
max | number | 5 | Maximum number of toasts kept at once; beyond that, the oldest card is removed |
useToast()
Returns { toast, dismiss }:
| Method | Signature | Description |
|---|---|---|
toast | (options: ToastOptions) => string | Sends a toast and returns its id |
dismiss | (id: string) => void | Manually closes a toast by id |
ToastOptions
| Field | Type | Default | Description |
|---|---|---|---|
title | string | — | Title |
description | string | — | Description text, shown below the title |
variant | 'default' | 'success' | 'error' | 'default' | Variant, deciding the leading edge color band and the icon (Info / CheckCircle / XCircle) |
How it works
- Collapsed stack: when not hovered, only the frontmost (newest) card stays in the document flow; the rest are absolutely positioned behind it, with
scaledecreasing by distance (-0.05 per layer) and a 14px offset toward the outside of the docked edge so a thin sliver peeks out. From the 4th card onward they are fully hidden (opacity 0 and no pointer events). - Hover to expand: when the pointer enters the viewport (or keyboard focus moves into any toast), the stack expands into the full list — the cards behind switch from absolute positioning back into the document flow, and Motion's layout FLIP springs them to their own positions; on leave, it collapses back into the stack in reverse.
- Enter and exit: new toasts slide in from the docked top or bottom edge (
y: ±120%plus a fade in) while the other cards make room with a layout animation; on exit they slide out toward the left or right edge, andAnimatePresence'spopLayoutmode lets the remaining cards close the gap in sync. - Countdown pausing: while hovered, every
setTimeoutis cleared; on leaving the viewport a full countdown is scheduled again for each toast (reset, not resumed). Whenever a toast is dismissed, pushed out bymax, or the Provider unmounts, its timer is always reclaimed. - Card spacing: when expanded, the spacing is implemented as padding on each card rather than
gapon the container, so moving the pointer between cards never breaks the hover state and the stack does not judder open and closed. - Viewport implementation: the viewport is a
pointer-events-nonecontainer spanning a full-height vertical corridor of the screen (events bubble up from the cards). Its on-screen position is fixed, so the absolute-position measurements taken during apopLayoutexit do not jump as the container height changes.
Accessibility
- The viewport carries
aria-live="polite"and each toast carriesrole="status", so new toasts are announced by the screen reader without interrupting what the user is doing - Every toast has a close button with an
aria-label; keyboard focus entering the viewport behaves like hover — the stack expands and the countdown pauses, resuming once focus leaves - When the user has "reduce motion" enabled at the system level, the slide in / slide out and the stack offsets all drop to zero and degrade to a brief fade in / fade out with no sense of movement
- Toasts never steal focus; after sending one, focus stays on whatever element the user was working with
Dynamic Island Toast
An iOS Dynamic Island-style notification — an idle pill expands fluidly into a notification card, multiple notifications stack behind it with their edges peeking out, and a loading state can update in place to success.
Cursor Tooltip
An elastic tooltip that follows the cursor, tracking its coordinates with a spring and flipping sides automatically near the edges, with keyboard focus anchoring.