Skip to content

Migrate from Sonner

import { toast, Toaster } from 'sonner';
import { toast, Toaster } from 'scrollsheet';

Drop-in: same toast, useSonner, Toaster exports, built on one non-modal Sheet.Root per <Toaster> instead of Sonner’s own DOM stack. toast(), .success(), .error(), .info(), .warning(), .loading(), .message(), .custom(jsx), .promise(), .dismiss(id?) all work before or after any <Toaster> mounts, backed by the same module-level queue. A repeated id updates a toast in place instead of stacking a second one. toasterId routes independently: a toast() call with a matching toasterId shows only on that instance, everything else on the default un-keyed one.

The stack shows a collapsed front card with ghost cards behind it, expanding to the full list on hover or focus, same as real Sonner. Auto-dismiss uses the toast’s own duration, or the Toaster’s. Bug fixed along the way: Sheet.Content moves focus onto the panel by default, which would steal focus every time a toast fired. This shim captures and restores focus around the mount so it never does.

Fire it a few times in a row and watch the queue stack, newest on top.

<Toaster visibleToasts={5}> caps how many toasts render at once, default 3 like Sonner’s own. Past the cap, the oldest dismissible toast evicts (onDismiss fires, same as a manual close) to make room for the new arrival. A dismissible: false toast holds its slot instead: if every visible slot is pinned that way, a new toast queues untouched in the store until one clears itself. visibleToasts clamps to a minimum of 1: 0 or a negative number still shows one toast, not zero.

Enter/exit, per-row swipe-to-dismiss (drag past 45px), and the crossfade between the collapsed and expanded stack are CSS transitions, not a JS animation loop, adapted from Sonner’s own approach. prefers-reduced-motion: reduce collapses every duration to zero and turns the transitions off outright, the same contract the core primitive already keeps.

Auto-injected styles land in document.head on first render, same as the core primitives. Import scrollsheet/sonner.css yourself to control load timing instead.