A stacked toast manager on Base UI Toast: a Toaster that mounts the stack, a useToast() hook, and helpers for toasting from outside React.
When to use
- Confirming something that just happened away from where the person is looking: "Deployed to production", "Invite sent".
- A reversible action with an Undo window: "Moved 3 issues to Done" with an Undo button.
- Work that finishes later: a promise toast shows a spinner, then morphs in place into the result.
- Errors that are not tied to one field, such as a background job or a failed sync.
When not to use
Installation
npx shadcn add https://eased.dev/r/toast.jsonThe command adds Toast, everything it builds on and Eased’s tokens to your project.
Mount one Toaster near the root of your app. Components inside it can call useToast().
import { Toaster } from "@/components/eased/toast/toast";export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Toaster>{children}</Toaster> </body> </html> );}Usage
import { Button } from "@/components/eased/button/button";import { useToast } from "@/components/eased/toast/toast";export function DeployButton({ deploy }: { deploy: () => Promise<{ seconds: number }> }) { const toast = useToast(); return ( <Button variant="primary" onClick={() => toast.promise(deploy(), { loading: { title: "Deploying northwind-web…", description: "Building main · 7d3e1b2" }, success: (r) => ({ title: "Deployed to production", description: `northwind.app · built in ${r.seconds}s` }), error: (e) => ({ title: "Deploy failed", description: String(e) }), }) } > Deploy </Button> );}Examples
Undo
action adds one button (an Eased Button, small, secondary). The toast closes after the click unless onClick returns false.
toast.add({ title: "Moved 3 issues to Done", description: "NW-412, NW-418 and NW-421", action: { label: "Undo", onClick: () => restore(ids) },});Success and error
toast.success("Invite sent", { description: "devon@northwind.io can now view “Q3 launch plan”." });toast.error("Build failed again", { description: "DATABASE_URL is missing in Preview." });A global manager
To toast from outside React (a fetch wrapper, a store), create a manager once and bind the helpers to it.
import { createToastApi, createToastManager } from "@/components/eased/toast/toast";export const toastManager = createToastManager();export const toast = createToastApi(toastManager);<Toaster toastManager={toastManager} />;toast.success("Saved");Inside a container
Pass container to portal the stack into an element instead of document.body. The stack is then absolute inside it, so give the element position: relative. The demo does this so its toasts land in its own frame.
API reference
Toaster
| Prop | Type | Default | Description |
|---|---|---|---|
position | "bottom-right" | "bottom-center" | "top-center" | "top-right" | "bottom-right" | Where the stack sits. Under 640px it is always full-width at the bottom, above the safe area. |
limit | number | 3 | Toasts shown at once. Older ones fade out (data-limited) and come back as slots free. |
timeout | number | 5000 | Default auto-dismiss in ms; 0 keeps toasts until closed. Loading toasts never time out. |
toastManager | ToastManager | A manager from createToastManager(), for toasting outside React. | |
container | HTMLElement | null | document.body | Portal target. When set, the stack is absolute inside it. |
swipeDirection | ToastSwipeDirection | ToastSwipeDirection[] | per position | Swipes that dismiss: down + right at the bottom, up + right top right, up top centre. |
closeLabel | string | "Dismiss" | Accessible name of each close button. |
className | string | "" | Appended to the viewport's classes. |
children | ReactNode | Your app; useToast() works inside it. |
useToast / createToastApi
useToast() (inside a Toaster) and createToastApi(manager) (anywhere) return the same methods. They are thin wrappers over Base UI's manager.
| Method | Description |
|---|---|
add(options) | Shows a toast and returns its id. An existing id updates that toast in place. |
success(title, options?) | A success toast with a check. |
error(title, options?) | An error toast with an alert glyph, announced urgently (priority: "high") unless you pass priority. |
promise(promise, { loading, success, error }) | One toast that follows the promise and morphs in place. Each state is a title or options; success and error may be functions of the result. Returns the promise. |
update(id, options) | Changes a toast in place; its timer restarts. |
close(id?) | Closes one toast, or all of them. |
ToastOptions
| Option | Type | Default | Description |
|---|---|---|---|
title | ReactNode | One short line. Promise toasts keep it to one line so it can swap. | |
description | ReactNode | Supporting text; wraps. | |
type | "default" | "success" | "error" | "loading" | "default" | Sets the icon. "default" has none, so the title starts at the edge. |
action | { label: string; onClick: () => void | false } | One action button, e.g. Undo. | |
timeout | number | the Toaster's | Per-toast auto-dismiss in ms. |
priority | "low" | "high" | "low" | "high" is announced at once (an alert). |
id | string | generated | Reuse an id to update a toast instead of adding one. |
data.icon | ReactNode | Replaces the status icon. | |
onClose, onRemove, actionProps | Base UI's options pass through. |
createToastManager re-exports Base UI's. Toasts carry data-type, so you can restyle a type from outside.
Keyboard
| Key | Action |
|---|---|
F6 | Moves focus into the toast region and expands the stack. |
Shift+Tab (on the region) | Returns focus to where it was before F6. |
Tab / Shift+Tab | Moves through the toasts and their action and close buttons. |
Enter / Space | Runs the focused action, or closes the toast from its close button. |
Esc | Closes the focused toast. |
Accessibility
- The viewport is a
regionlandmark named "Notifications" and a polite live region, so new toasts and their text changes (a promise resolving) are announced.priority: "high"toasts (errors by default) are announced through analertinstead. - Each toast is a non-modal
dialoglabelled by its title and described by its description. - Hover, keyboard focus inside the stack and a hidden window all pause every timer, so nothing disappears while someone reads or reaches for Undo.
- The close button appears on hover with a fine pointer and on keyboard focus, and always shows on touch screens; it keeps its space when hidden, so nothing shifts. Touch users can also swipe a toast away.
- Toasts past
limitareinertuntil they return.
Motion
A toast rises 12px at 0.97 into place on the smooth spring (--eased-spring-smooth, 450ms) with a 200ms opacity tween; it leaves half the distance and half the scale in 150ms on the standard curve, never overshooting. Swiped away, it carries on in the swipe's direction from where the finger let go. Behind the newest, toasts peek 10px and shrink 5% each, scaled from the stack's anchored edge, with their content faded out; hovering or focusing fans the stack out 8px apart on the same spring, and every move is interruptible. No height is animated: each toast keeps its natural height, and a taller toast behind slides its card layer to the front toast's height with a clip that sits behind the front. A promise toast morphs in place: the spinner cross-fades and scales (0.6 → 1, snappy) into a check that draws its stroke in, or an alert glyph, while the title swaps with Text Swap. Reduced motion keeps only opacity: positions change at once and fades take 100ms.
Notes for AI
Mount one Toaster at the app root (or use a global manager with createToastApi), then call useToast() where things happen. Use promise for anything async rather than adding a loading toast and closing it, so the toast morphs instead of replacing itself; use action for Undo. Keep titles to one short line and put detail in description.
Do not wrap toasts in your own animation, timers or aria-live region: Base UI pauses timers, owns swipe and announces toasts, and the stack motion is already there. Do not use a toast for form validation or for anything that must be acknowledged; use inline errors or a Dialog.