Migrate from Sonner
import { toast, Toaster } from 'sonner';import { toast, Toaster } from 'scrollsheet';Drop-in: same toast, useSonner, Toaster exports, running on the same architecture real
Sonner uses: a persistent card per toast, all portaled through one shared <section> in
document.body, positioned with CSS instead of stacked inside a shared sheet panel.
Every method (toast(), .success(), .error(), .info(), .warning(), .loading(),
.message(), .custom(jsx), .promise(), .dismiss(id?)) works before or after a
<Toaster> mounts, backed by one shared module-level queue.
CSS compatibility
Section titled “CSS compatibility”What maps 1:1
Section titled “What maps 1:1”Fire it a few times in a row and watch the queue stack, newest on top.
| Behavior | Detail |
|---|---|
Repeated id |
Updates that toast in place instead of stacking a duplicate |
toasterId |
A toast() call with a matching toasterId shows only on that instance; everything else lands on the default un-keyed one |
position |
All six Sonner positions render; a toast’s own position overrides the Toaster’s default for just that one toast |
| Stack | Every toast is a real, persistent card, not a ghost placeholder; collapsed ones sit behind the front one and expand to the full list on hover or Alt+T (configurable via hotkey). Escape dismisses the front toast, or collapses the stack back if the hotkey opened it, though a stack the pointer is still hovering stays expanded until the pointer leaves |
visibleToasts |
Caps how many toasts show at once, default 3 like Sonner. Toasts past the cap stay mounted but hidden, timers still running, and fade in the moment a slot frees; nothing gets evicted. Minimum 1: 0 or negative still shows one |
| Auto-dismiss | Uses the toast’s own duration, or the Toaster’s. Pauses on hover, any pointer down on the stack (including mid-swipe), keyboard expand, and a hidden tab, resuming with the time left rather than a fresh countdown; updating a live toast’s duration re-arms the timer from the new value |
| Action / cancel buttons | Dismiss after their own onClick runs: cancel unconditionally, action unless the handler calls event.preventDefault() |
type: 'loading' |
Can’t be swiped or closed by button, regardless of dismissible |
toast.promise() |
Accepts an extended result object ({ message, ...rest }) from success/error; a static description set at call time survives to the settled toast |
toast.getToasts() / .getHistory() |
Mirror Sonner’s own introspection API; history is capped at 100 entries |
testId, closeButtonAriaLabel |
Render as data-testid; override the close button’s accessible name |
Per-toast style |
toast('Saved', { style: { top: '8px' } }) puts inline styles on that toast’s row, merged over the stack’s own offset and scale variables, so position and zIndex overrides stick. toastOptions.style on the Toaster sets the base for every row |
Swipe and motion
Section titled “Swipe and motion”Swipe-to-dismiss tracks both axes: drag past 45px or flick it fast enough and it dismisses,
release short of that and it springs back. Enter/exit, the stack’s expand/collapse, and the
swipe itself are all CSS transitions, not a JS animation loop; prefers-reduced-motion: reduce
zeroes all of them, the same contract the core primitive keeps everywhere else.
What changes
Section titled “What changes”Styles setup
Section titled “Styles setup”Sonner injects its styles automatically; scrollsheet’s default entry expects two imports next
to the Toaster instead: scrollsheet/styles.css and scrollsheet/toast.css.
For sonner’s zero-setup behavior, import toast and Toaster from scrollsheet/auto:
embedded styles, injected on first render. The auto entry is also the right one for design
systems whose CSS pipeline statically compiles custom properties away (postcss-css-variables
and similar); that compilation destroys the runtime variables the stylesheets’ geometry runs on.