WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/toast-stack.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

5000
5
<ToastStack />

Installation

npx shadcn@latest add https://webberui.com/r/toast-stack.json

Or, 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

PropTypeDefaultDescription
childrenReact.ReactNodeApp 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
durationnumber5000Milliseconds before auto-dismiss; set to 0 or below, or Infinity, to disable auto-dismiss
maxnumber5Maximum number of toasts kept at once; beyond that, the oldest card is removed

useToast()

Returns { toast, dismiss }:

MethodSignatureDescription
toast(options: ToastOptions) => stringSends a toast and returns its id
dismiss(id: string) => voidManually closes a toast by id

ToastOptions

FieldTypeDefaultDescription
titlestringTitle
descriptionstringDescription 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 scale decreasing 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, and AnimatePresence's popLayout mode lets the remaining cards close the gap in sync.
  • Countdown pausing: while hovered, every setTimeout is cleared; on leaving the viewport a full countdown is scheduled again for each toast (reset, not resumed). Whenever a toast is dismissed, pushed out by max, or the Provider unmounts, its timer is always reclaimed.
  • Card spacing: when expanded, the spacing is implemented as padding on each card rather than gap on 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-none container 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 a popLayout exit do not jump as the container height changes.

Accessibility

  • The viewport carries aria-live="polite" and each toast carries role="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

On this page