Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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
stable
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/toast

Usage

<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
ToastViewport — data-slot="toast-viewport"
ToastRoot — data-slot="toast"
ToastContent — data-slot="toast-content"
ToastTitle — data-slot="toast-title"
ToastDescription — data-slot="toast-description"
ToastAction — data-slot="toast-action"
ToastClose — data-slot="toast-close"
ToastPositioner — data-slot="toast-positioner"
ToastArrow — data-slot="toast-arrow"
Toast — data-slot="toast-icon"
Toaster
<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

PropTypeDefaultDescription
toast(title, options?)(title, options?) => stringShow a neutral toast. Returns the toast id. Passing an id that already exists updates that toast in place.
toast.success(title, options?)(title, options?) => stringSuccess toast — success tint + CircleCheck icon.
toast.error(title, options?)(title, options?) => stringError toast — destructive tint + XCircle icon. Announced urgently.
toast.warning(title, options?)(title, options?) => stringWarning toast — warning tint + AlertTriangle icon. Announced urgently.
toast.info(title, options?)(title, options?) => stringInfo toast — info tint + Info icon.
toast.loading(title, options?)(title, options?) => stringPending 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 }) => PromiseDrive 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?) => stringRender 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) => voidUpdate a live toast in place and refresh its timer. Changing the type re-derives the announcement priority.
toast.dismiss(id?)(id?: string) => voidDismiss 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

PropTypeDefaultDescription
childrenReact.ReactNodeThe application subtree that can fire toasts.
limitnumber3How 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.
timeoutnumber5000Milliseconds before a toast auto-dismisses. 0 disables auto-dismiss. The loading type never auto-dismisses regardless.
toastManagerToastManager<ToastData>toastManagerThe 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

PropTypeDefaultDescription
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

AttributeValues
data-slot"toast-viewport"

ToastRootProps

PropTypeDefaultDescription
toast*ToastItemThe 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

AttributeValues
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

PropTypeDefaultDescription
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

AttributeValues
data-slot"toast-action"

ToastCloseProps

PropTypeDefaultDescription
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

AttributeValues
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

PropTypeDefaultDescription
toast*ToastItemThe toast to render.
anchor"bottom" | "top"Which edge the stack grows from — sets origin so the collapsed scale reads right.
closeButtonbooleantrueShow the dismiss X. Off only for toasts that must be resolved by their action.
closeLabelstring'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

AttributeValues
data-slot"toast-icon"

ToasterProps

PropTypeDefaultDescription
closeButtonbooleantrueShow 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` bodyRender 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

KeyEffect
F6Move focus into the notifications viewport from elsewhere on the page.
Tab / Shift+TabReach the labelled action and the dismiss X.
Enter / SpaceActivate the focused action or dismiss control.
EscapeDismiss the toast that currently has focus.
  • The viewport is a labelled landmark region (role="region", aria-label="Notifications") with aria-live="polite", so a new toast is announced without stealing focus.
  • Live-region policy: error and warning toasts are announced urgently — Base UI renders a visually hidden role="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; pass priority explicitly to override.
  • F6 jumps focus into the viewport from anywhere on the page, and Escape dismisses the toast that has focus. Each toast is a focusable role="dialog" (or alertdialog when 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.
ContractStates tested
Behaviourdefault, success, error, warning, info, loading, expanded, swiping, limited
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation, live-region
Visualdefault, hover, focus, success, error, warning, info, loading

Do / Don't

Do
Use Toast for brief, non-blocking confirmations — 'Saved', 'Copied', 'Invitation sent' — and for the loading → success arc of an async action.
Don't
Use Toast for errors the user must resolve or destructive confirmations — use an inline message or a Dialog.
Do
Give a reversible result a labelled Undo action when the user can still change the outcome.
Don't
Rely on colour or swipe alone to convey status or dismiss a notification.

On this page