Toast
Brief, non-blocking notifications — a stacking Base UI Toast surface with success, error, warning, info and loading types, promise toasts and swipe-to-dismiss.
- Status
- Since
0.7.0- Accessibility pattern
- labelled live region
Last updated
Install
Add Toast from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/toastUsage
<VegaStackProvider> mounts the toast host. Import toast and call it from anywhere, including
outside the React tree:
import { toast } from "@/components/ui/toast";
<Button onClick={() => toast.success("Saved")}>Save</Button>;Scope
- Owns: brief notifications, their stack, placement, live announcements, and dismissal.
- Does not own: errors that need an inline resolution or a confirmation before a destructive action.
- Compose with:
<VegaStackProvider>for the usual app root; otherwise mount<ToastProvider>and one<Toaster />yourself.
Toast uses Base UI's Toast primitive. If you do not
use <VegaStackProvider>, mount the pair once at your app root:
import { ToastProvider, Toaster } from "@/components/ui/toast";
export default function RootLayout({ children }) {
return (
<body>
<ToastProvider>
{children}
<Toaster />
</ToastProvider>
</body>
);
}Anatomy
The default <Toast> assembles these parts; export them individually to build your own list.
<ToastProvider>
<ToastPortal>
<ToastViewport>
<ToastRoot toast={item}>
<ToastContent>
<ToastTitle />
<ToastDescription />
<ToastAction />
<ToastClose />
</ToastContent>
</ToastRoot>
</ToastViewport>
</ToastPortal>
</ToastProvider>Examples
Types
toast.success / toast.error / toast.warning / toast.info carry a matching lucide icon and
the family's tinted surface — the same recipe Alert uses, so the two status surfaces read as one
design. The bare toast() is neutral.
The type strings follow Base UI's engine rather than our token families, which is why the
destructive type is spelled error: Base UI's own promise() writes type: "loading" and then
type: "success" | "error", and its auto-dismiss timer is keyed off the loading string. The
tokens still follow the house families — an error toast paints destructive.
Description, action and promise
A toast takes a description, an actionProps button, and toast.promise(...) to drive
loading → success / error from an async call automatically.
The labelled action and the dismiss X share a quiet ghost face and family-coloured hover/pressed
wash; the action remains distinct through its text label.
toast.success("File uploaded", { description: "2.5 MB saved" });
toast("Invitation sent", {
description: "sent to jane@vegastack.com",
actionProps: { children: "Undo", onClick: () => toast("Invitation revoked") },
});
// Resolves/rejects with the original promise, so it composes with `await`.
await toast.promise(saveData(), {
loading: "Saving…",
success: "Saved!",
error: "Save failed",
});Loading toasts
toast.loading(...) shows a spinner and returns the toast id. A loading toast never
auto-dismisses — that is Base UI's timer rule, not a duration we picked — so you must resolve it:
call toast.update with the same id to swap the type in place, or toast.dismiss to drop it.
const id = toast.loading("Uploading file…");
await uploadFile();
toast.update(id, { type: "success", title: "File uploaded" });Stacking
Toasts stack collapsed, scaled and peeking behind the frontmost one, and expand to their natural
heights when the viewport is hovered or focused. Past the provider's limit (3 by default) older
toasts stay mounted but inert so they can animate out rather than vanish.
Passing an id that already exists updates that toast in place and refreshes its timer, instead of
stacking a duplicate — the fix for a retry loop that would otherwise fire twenty identical toasts.
Custom body
toast.custom((item) => …) replaces the toast's body while keeping everything that makes it a
toast: stacking, swipe-to-dismiss, Escape, and the live-region announcement.
toast.custom((item) => <PlanNotice onDismiss={() => toast.dismiss(item.id)} />);Timing and lifetime
Mount exactly one <Toaster /> per app (usually via <VegaStackProvider>). Host-level
defaults — position, limit, timeout — live on that single mount; per-toast overrides go on
the toast() call. timeout: 0 disables auto-dismiss for one toast.
// App root — the single mount sets the defaults…
<ToastProvider limit={5} timeout={8000}>
{children}
<Toaster position="top-end" />
</ToastProvider>;
// …and a specific toast can override per call:
toast("Deployment queued", { timeout: 15000 });Position and RTL
position names are logical on the inline axis: bottom-end is bottom-right in an LTR document
and bottom-left in an RTL one, so a single value is correct in both. The stack's growth direction,
transform origin, and enter/exit travel all follow from it.
Swipe directions default to the stack's own block direction plus both inline directions, which
reads the same under either text direction. Pass swipeDirection to narrow it.
Reduced motion
The enter, exit, stack and expand transitions are CSS transitions, so the design system's global
prefers-reduced-motion reset neutralizes them: a toast appears and disappears without travel. The
loading spinner carries motion-reduce:animate-none. Nothing about dismissal depends on an
animation completing.
Playground
Compose a type and an optional description, fire the toast, then copy the matching call.
toast("Event created");API Reference
toast(title, options?) is an imperative function, so its methods are documented by hand below.
options is Base UI's add-options object: description, actionProps, timeout, priority,
id, onClose, data, and the rest.
toast function
| Prop | Type | Default | Description |
|---|---|---|---|
toast(title, options?) | (title, options?) => string | — | Show a neutral toast. Returns the toast id. Passing an id that already exists updates that toast in place. |
toast.success(title, options?) | (title, options?) => string | — | Success toast — success tint + CircleCheck icon. |
toast.error(title, options?) | (title, options?) => string | — | Error toast — destructive tint + XCircle icon. Announced urgently. |
toast.warning(title, options?) | (title, options?) => string | — | Warning toast — warning tint + AlertTriangle icon. Announced urgently. |
toast.info(title, options?) | (title, options?) => string | — | Info toast — info tint + Info icon. |
toast.loading(title, options?) | (title, options?) => string | — | Pending toast with a spinner. Never auto-dismisses; resolve it with toast.update or toast.dismiss using the returned id. |
toast.promise(promise, options) | (promise, { loading, success, error }) => Promise | — | Drive one toast through loading → success / error from an async call. Resolves/rejects with the original promise's value. |
toast.custom(render, options?) | ((item) => ReactNode, options?) => string | — | Render the toast body yourself inside a real toast — stacking, swipe, Escape and the live region all still apply. |
toast.update(id, options) | (id: string, options) => void | — | Update a live toast in place and refresh its timer. Changing the type re-derives the announcement priority. |
toast.dismiss(id?) | (id?: string) => void | — | Dismiss a toast by id, or every toast when called with no argument. |
useToast
useToast() is Base UI's useToastManager typed to our toast data. It returns the reactive
toasts array plus add / close / update / promise — use it to render your own list. Firing
a toast needs no hook.
ToastProviderProps
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The application subtree that can fire toasts. |
limit | number | 3 | How many toasts render at once. Older toasts past the limit stay mounted, marked
data-limited (and inert), so they can animate out rather than vanish. |
timeout | number | 5000 | Milliseconds before a toast auto-dismisses. 0 disables auto-dismiss.
The loading type never auto-dismisses regardless. |
toastManager | ToastManager<ToastData> | toastManager | The manager toasts are queued into. Defaults to the module-scope toastManager,
which is what makes the imperative toast() work from outside the React tree. Pass your
own only when you need a second, isolated toast channel. |
ToastViewportProps
| Prop | Type | Default | Description |
|---|---|---|---|
position | "bottom-center" | "bottom-end" | "bottom-start" | "top-center" | "top-end" | "top-start" | 'bottom-end' | Logical corner for this viewport's stack. |
Data attributes and CSS variables on ToastViewport
| Attribute | Values |
|---|---|
data-slot | "toast-viewport" |
ToastRootProps
| Prop | Type | Default | Description |
|---|---|---|---|
toast* | ToastItem | — | The toast to render. |
anchor | "bottom" | "top" | — | Which edge the stack grows from — sets origin so the collapsed scale reads right. |
type | "default" | "error" | "info" | "loading" | "success" | "warning" | 'default' | Surface tint, normally inferred from the toast item. |
Data attributes and CSS variables on ToastRoot
| Attribute | Values |
|---|---|
data-slot | "toast" |
ToastContent
ToastContent adds no own props beyond Base UI's content part.
ToastTitle
ToastTitle adds no own props beyond Base UI's title part.
ToastDescription
ToastDescription adds no own props beyond Base UI's description part.
ToastActionProps
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "brand" | "destructive" | "foreground" | "info" | "primary" | "success" | "warning" | 'foreground' | Which ink the hover/pressed wash composites from. Toast sets it from the toast's type, so a
control inside a tinted toast washes in its own family rather than a borrowed neutral. |
Data attributes and CSS variables on ToastAction
| Attribute | Values |
|---|---|
data-slot | "toast-action" |
ToastCloseProps
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "brand" | "destructive" | "foreground" | "info" | "primary" | "success" | "warning" | 'foreground' | Which ink the hover/pressed wash composites from. Toast sets it from the toast's type. |
Data attributes and CSS variables on ToastClose
| Attribute | Values |
|---|---|
data-slot | "toast-close" |
ToastPositioner
ToastPositioner adds no own props beyond Base UI's anchored positioner.
ToastArrow
ToastArrow adds no own props beyond Base UI's anchored arrow.
ToastPortal
ToastPortal adds no own props beyond Base UI's portal.
ToastProps
| Prop | Type | Default | Description |
|---|---|---|---|
toast* | ToastItem | — | The toast to render. |
anchor | "bottom" | "top" | — | Which edge the stack grows from — sets origin so the collapsed scale reads right. |
closeButton | boolean | true | Show the dismiss X. Off only for toasts that must be resolved by their action. |
closeLabel | string | 'Dismiss notification' | Accessible name for the dismiss control. |
type | "default" | "error" | "info" | "loading" | "success" | "warning" | 'default' | Surface tint, normally inferred from the toast item. |
Data attributes and CSS variables on Toast
| Attribute | Values |
|---|---|
data-slot | "toast-icon" |
ToasterProps
| Prop | Type | Default | Description |
|---|---|---|---|
closeButton | boolean | true | Show the dismiss X on every toast. |
position | "bottom-center" | "bottom-end" | "bottom-start" | "top-center" | "top-end" | "top-start" | 'bottom-end' | Which corner the stack pins to. Inline names are logical, so bottom-end is bottom-right in
an LTR document and bottom-left in an RTL one. |
renderToast | ((item: ToastItem) => React.ReactNode) | undefined — the assembled `Toast` body | Render every toast's body yourself. A per-toast data.render (from toast.custom()) wins
over this. Named renderToast because render is Base UI's polymorphic element prop, which
the viewport keeps. |
swipeDirection | "down" | "left" | "right" | "up" | ("down" | "left" | "right" | "up")[] | ['down' | 'up', 'left', 'right'] — the block direction follows `position` | Direction(s) a toast can be swiped to dismiss. Defaults to the stack's own block direction plus both inline directions, which reads the same under either text direction. |
Accessibility
| Key | Effect |
|---|---|
F6 | Move focus into the notifications viewport from elsewhere on the page. |
Tab / Shift+Tab | Reach the labelled action and the dismiss X. |
Enter / Space | Activate the focused action or dismiss control. |
Escape | Dismiss the toast that currently has focus. |
- The viewport is a labelled landmark region (
role="region",aria-label="Notifications") witharia-live="polite", so a new toast is announced without stealing focus. - Live-region policy:
errorandwarningtoasts are announced urgently — Base UI renders a visually hiddenrole="alert"mirror of their title and description; every other type stays polite. This is derived from the type, so callers get it right by default; passpriorityexplicitly to override. F6jumps focus into the viewport from anywhere on the page, andEscapedismisses the toast that has focus. Each toast is a focusablerole="dialog"(oralertdialogwhen urgent), so the action and dismiss controls are reachable without a pointer.- Status types pair colour with an icon, so meaning is never carried by colour alone; the surface uses semantic tokens and its contrast is gated in both themes by the compiled-CSS suite.
- Swipe-to-dismiss is an addition, never the only route: every toast keeps a dismiss control and Escape.
- The dismiss control is 24px, meeting the pointer-target floor without an extra hit area.
| Contract | States tested |
|---|---|
| Behaviour | default, success, error, warning, info, loading, expanded, swiping, limited |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation, live-region |
| Visual | default, hover, focus, success, error, warning, info, loading |