Skip to content
Monogem

Component · Navigation & Feedback

Toast

Overview

A short, transient message that reports the result of an action without interrupting what the user is doing — "Draft saved", "Invite sent", "Upload failed". A Toast appears in a fixed corner of the screen (bottom-right by default, or top-right), stacks with any others, and (for non-error intents) dismisses itself after a few seconds.

Toast is the transient counterpart to Alert. If the message belongs in the flow of the page and should stay until the surrounding content changes, that is an Alert. If it needs a title, a body, and a form, that is a Dialog. Toast is for feedback that is safe to miss and safe to dismiss.

Toast is an imperative, queued notification: a provider renders the viewport, toast() adds a message, timers auto-dismiss it (pausing while the user is interacting with it), and each message is announced through a live region — see Accessibility.

Anatomy

ToastProvider                 (client root: the queue, the timers, the portalled viewport)
└─ ToastViewport              <ol role="region" aria-label="Notifications" tabIndex=-1>
   └─ Toast                   <li> — one notification
      ├─ (live region)        role="status" | "alert", aria-atomic — holds:
      │  ├─ intent icon       decorative Lucide glyph (none for `default`)
      │  ├─ (sr-only word)    "Note:" / "Success:" / "Warning:" / "Error:"
      │  ├─ ToastTitle        the heading
      │  ├─ ToastDescription  optional supporting line
      │  └─ ToastAction       optional single button — running it also dismisses
      └─ ToastClose           the ✕ — always present

Callers never render Toast / ToastViewport directly. They mount one ToastProvider and call the queue:

<ToastProvider duration={5000} max={3} position="bottom-right">
{app}
</ToastProvider>

// anywhere below it
const { toast, dismiss } = useToast();
const id = toast({
intent: "success",
title: "Invite sent",
description: "We emailed the invitation to your teammate.",
action: <ToastAction>Undo</ToastAction>,
});
dismiss(id); // one
dismiss(); // all
  • ToastProvider — holds the queue and every timer, and portals the viewport to <body>. duration (default 5000 ms) is the auto-dismiss delay for default / info / success; max (default 3) is how many toasts are visible at once; position ("bottom-right" default, or "top-right") is where the stack anchors — provider-level, never per toast.
  • useToast() — returns { toast, dismiss }. Throws if called outside a ToastProvider. There is no module-level toast() singleton in Version 1.
  • toast(options) — enqueues one toast and returns its id. options: intent (default default), title (required), description, action, duration (ms, or Infinity to persist — overrides the per-intent default).
  • ToastAction — the one optional action, styled as a small outline button. Running it runs its onClick and then dismisses the toast.
  • ToastClose — the ✕, rendered automatically on every toast (aria-label="Dismiss"). A Toast is dismissible by contract; Alert is not.
  • ToastViewport — the role="region" landmark that holds the toasts. Not the live region itself — each toast carries its own.

Live Example

Position

Intents

With an action

Stacking & the FIFO queue

Code Example

toast-showcase.tsx
"use client";

import * as React from "react";

import { Button } from "@/components/ui/button";
import {
ToastAction,
ToastProvider,
useToast,
type ToastIntent,
type ToastPosition,
} from "@/components/ui/toast";

/**
* Live Toast example. `"use client"`, and the demo mounts its own
* `<ToastProvider>` (an app would mount one at the root). Toasts portal to a
* fixed viewport at a corner of the window — not inside this card — so this is
* the real placement, not a mock.
*
* - One button per intent. `default` / `info` / `success` auto-dismiss after
* 5s, `warning` after 8s; `destructive` stays until dismissed.
* - "Message archived" carries a `<ToastAction>` — running it (or the ✕)
* dismisses the toast.
* - "Notify ×5" fires five at once: three show, the rest wait in a FIFO queue
* and appear as the visible ones clear. Hover or focus the stack to pause
* every countdown.
* - The position toggle switches the provider between the two V1 placements —
* `bottom-right` (stacks upward) and `top-right` (stacks downward).
*/
const INTENTS: { intent: ToastIntent; label: string; title: string; description: string }[] = [
{
intent: "default",
label: "Neutral",
title: "Draft saved",
description: "Your changes are stored locally.",
},
{
intent: "info",
label: "Info",
title: "Sync scheduled",
description: "This workspace will sync in a few minutes.",
},
{
intent: "success",
label: "Success",
title: "Invite sent",
description: "We emailed the invitation to your teammate.",
},
{
intent: "warning",
label: "Warning",
title: "Storage almost full",
description: "You have used 92% of your plan's storage.",
},
{
intent: "destructive",
label: "Error",
title: "Upload failed",
description: "The connection dropped. Nothing was uploaded.",
},
];

function ToastDemo({
position,
onPositionChange,
}: {
position: ToastPosition;
onPositionChange: (next: ToastPosition) => void;
}) {
const { toast, dismiss } = useToast();

return (
<div className="flex flex-col gap-6">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Position
</h3>
<div className="flex flex-wrap gap-2">
{(["bottom-right", "top-right"] as const).map((value) => (
<Button
key={value}
variant={position === value ? "secondary" : "ghost"}
aria-pressed={position === value}
onClick={() => onPositionChange(value)}
>
{value}
</Button>
))}
</div>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Intents
</h3>
<div className="flex flex-wrap gap-2">
{INTENTS.map(({ intent, label, title, description }) => (
<Button
key={intent}
variant="outline"
onClick={() => toast({ intent, title, description })}
>
{label}
</Button>
))}
</div>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
With an action
</h3>
<Button
variant="outline"
onClick={() =>
toast({
intent: "success",
title: "Message archived",
action: <ToastAction>Undo</ToastAction>,
})
}
>
Archive message
</Button>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Stacking &amp; the FIFO queue
</h3>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={() => {
for (let i = 1; i <= 5; i += 1) {
toast({
intent: "info",
title: `Notification ${i}`,
description: "Three show at once; the rest queue.",
});
}
}}
>
Notify ×5
</Button>
<Button variant="ghost" onClick={() => dismiss()}>
Dismiss all
</Button>
</div>
</section>
</div>
);
}

export function ToastShowcase() {
const [position, setPosition] =
React.useState<ToastPosition>("bottom-right");

return (
<ToastProvider position={position}>
<ToastDemo position={position} onPositionChange={setPosition} />
</ToastProvider>
);
}

Variants

One axis: intent, five values — identical to Alert: same icon per intent, same visually hidden intent word, same --{intent}-subtle surfaces.

IntentSurfaceBorderTitle + iconBodyIconHidden word
default--popover--border--popover-foreground--popover-foregroundnone—
info--info-subtle--info-subtle-border--info-subtle-foreground--foregroundInfo"Note:"
success--success-subtle--success-subtle-border--success-subtle-foreground--foregroundCircleCheck"Success:"
warning--warning-subtle--warning-subtle-border--warning-subtle-foreground--foregroundTriangleAlert"Warning:"
destructive--destructive-subtle--destructive-subtle-border--destructive-subtle-foreground--foregroundOctagonAlert"Error:"
  • The neutral toast uses --popover, not Alert's --card: a Toast floats over arbitrary page content, and --popover is the system's floating-surface pair ("menus, tooltips, popovers"). Everything else matches Alert so the two read as siblings.
  • The four feedback intents differ by icon shape and word, not colour alone — same rule as Alert. Only the title takes the intent colour; body copy stays --foreground.
  • --shadow-lg is what signals "floating" — Shadows names it for "modals, dialogs, sheets", and a Toast is a peer of those.
  • No size, tone, or density variant.

States

Toast

StateBehaviour
Enteringopacity 0 → 1 with a small upward translate over ~150 ms (ease-out). Under prefers-reduced-motion there is no transition — it appears in place. The title / description are in the DOM from the first frame so the live region announces the full message.
VisibleOn the --{intent}-subtle (or --popover) surface, --shadow-lg, --radius-lg, --scale-4 padding. The auto-dismiss countdown is running (unless the intent persists, or the stack is paused).
PausedThe countdown for every visible toast is held while the pointer is over the viewport, while focus is inside it, or while the browser tab is hidden. It resumes from the remaining time.
QueuedBeyond max (default 3), a toast waits in a FIFO queue and is not rendered. Its countdown has not started.
LeavingRemoved immediately on dismiss — no exit animation in Version 1.

Timing per intent

IntentDefault auto-dismiss
default / info / successthe provider duration — 5000 ms by default
warning8000 ms
destructivepersists until dismissed

A caller duration (a number of ms, or Infinity) overrides the default for that toast.

Dismissal

ToastClose (✕), the auto-dismiss timer, running a ToastAction, Escape while focus is in the viewport, or a programmatic dismiss(id) / dismiss(). There is no swipe gesture in Version 1.

Placement

Set on the provider, never per toast — position has exactly two Version 1 values:

positionDesktop (--breakpoint-sm+)Stack directionMobile
bottom-right (default)Anchored to the bottom-right corner, --scale-4 from each edgeGrows upward — newest nearest the bottomFull-width near the bottom, --scale-4 inset each side
top-rightAnchored to the top-right corner, --scale-4 from each edgeGrows downward — newest nearest the topFull-width near the top, --scale-4 inset each side

Each toast is max-w-sm on desktop (no dedicated width token — Q5); the --scale-4 viewport inset and the toast sizing are the same for both positions. There is no top-left, bottom-left, centre, or arbitrary-offset placement in Version 1.

Stacking

max (default 3) toasts are visible, newest nearest the anchored edge. A sixth toast() call while three are showing and two are queued joins the back of the queue; a visible toast is never silently evicted to make room. A toast's countdown starts when it becomes visible, so a queued toast still gets its full time on screen.

Usage Guidance

Known limitation: toasts and modal layers

A toast fired while a Dialog, Sheet, or command palette is open is not supported. Everything behind an open modal layer — the Toast viewport included — is inert, so:

  • The toast is drawn underneath the scrim, covered and dimmed.
  • It cannot be focused, clicked, or dismissed, and it is not announced by assistive tech.
  • The auto-dismiss countdown keeps running (it pauses only on pointer-over, focus-within, and a hidden tab), so a default-duration toast can expire before the modal closes and never be seen. Persistent toasts (warning / destructive, or duration: Infinity) survive and become fully interactive again once the modal closes.

What to do instead: close the modal first and then call toast() (for example in the confirm handler, after closing), or report the result inline inside the modal with an Alert or a field-level error.

Tokens

Toast reuses the feedback colour family Alert uses — --{intent}-subtle / --{intent}-subtle-foreground / --{intent}-subtle-border for info / success / warning / destructive — plus --popover for the neutral surface, --shadow-lg for elevation, and existing --border / --radius-lg / --ring / --scale-*. It defines no toast-* tokens. See Color — Feedback.

TokenWhere usedRationale
--{intent}-subtle / -subtle-foreground / -subtle-borderthe four feedback intents — surface / title + icon / edgeThe feedback family Alert defined; Toast is the second documented consumer (Color)
--popover / --popover-foregrounddefault (neutral) surface + textColor names this pair for floating surfaces — a Toast floats
--borderdefault edgeThe system hairline
--foregroundbody copy on every feedback intentSame as Alert — a coloured body paragraph reads poorly
--shadow-lgelevationShadows — "modals, dialogs, sheets"; a Toast is a peer
--radius-lgcornerRadius — "cards, popovers"; matches Alert
--ringfocus ring on ToastAction / ToastCloseThe system focus ring, via buttonVariants
--scale-4toast padding; viewport inset from the screen edgeSpacing
--scale-3gap between stacked toasts; icon-to-content gapThe same 12px rhythm Alert uses
--scale-5intent-icon glyph size (20px)Icons — "default UI" size, matching Alert
--scale-8the desktop viewport width allowance around a max-w-sm toastSpacing
--scale-pxtoast border widthColor border recipe

Contrast is inherited from the feedback family, measured for Alert (WCAG 2.1 AA): body --foreground on each --{intent}-subtle is 15–19:1; title / icon --{intent}-subtle-foreground on the same surface is 5.8–11.1:1. --popover-foreground on --popover is near-maximal in both themes.

Do / Don’t

Do

  • Use a Toast for feedback that is safe to miss: a confirmation, a background result, a recoverable error with an Undo.
  • Keep the title to a few words and the description to one sentence. A Toast is glanceable.
  • Match the intent to the message, exactly as with Alert — success for a completed action, destructive for a failure, warning for a consequence, info for a neutral note.
  • Pair a destructive action with a ToastAction Undo where you can, instead of a blocking confirmation.
  • Let warning and destructive persist (the default) so the user actually sees them.

Don’t

  • Don't put anything required in a Toast — no "click here to keep your work", no form, no long copy. If it can't be missed, it isn't a Toast (Alert or Dialog).
  • Don't stack more than one action into a toast, or a ToastClose plus two buttons — the ✕ and one ToastAction is the whole budget.
  • Don't fire a toast on every keystroke or poll tick. Debounce, or collapse repeats.
  • Don't move focus to a Toast when it appears — it must never interrupt (see Accessibility).
  • Don't rely on colour alone — the icon and the intent word carry the meaning, keep them.
  • Don't fire a toast from inside an open Dialog or Sheet — see the known limitation below.

Accessibility

  • Non-modal, never steals focus. Showing a Toast does not move focus, add a scrim, mark the page inert, trap focus, or lock scroll — the defining contrast with Dialog. The page stays fully operable underneath.
  • Announcement. Each toast's icon + intent word + title + description sit in a live region: role="status" with aria-live="polite" for default / info / success; role="alert" with aria-live="assertive" for warning / destructive. aria-atomic="true" so the whole message is read, not a diff. The text is present at insertion (only opacity / transform animate), so the announcement is not truncated.
  • The viewport is a landmark. ToastViewport is role="region" with aria-label="Notifications" and tabIndex={-1}, so a keyboard or screen-reader user can move to accumulated toasts. It is placed last in <body>, so Tab reaches the toasts' controls after the page content.
  • Keyboard. Tab / Shift+Tab move through each toast's ToastAction and ToastClose in order — no roving tabindex, no trap. Escape, while focus is within the viewport, dismisses the toast that contains focus (or the newest, if focus is on the region itself); it is handled on the viewport, never as a global document listener.
  • Focus on dismiss. Dismissing a toast from the keyboard moves focus to the next toast's ✕, or the viewport, or the element that was focused before focus entered the viewport — it never drops to nowhere. Dismissal by the timer, a pointer, or a dismiss() call leaves focus untouched.
  • Timing is adjustable (WCAG 2.2.1). The countdown pauses on pointer-over, on focus-within, and while the tab is hidden, and resumes from the time remaining. warning and destructive do not auto-dismiss at all. A caller can pass duration: Infinity to make any toast persist.
  • Motion. The ~150 ms enter transition is disabled under prefers-reduced-motion (motion-reduce:). There is no looping animation and no exit animation.
  • Colour is never the only signal — a distinct icon shape per intent plus the visually hidden intent word, exactly as Alert.

Related Components / Patterns

  • Alert — a persistent, non-dismissible callout.
  • Dialog — modal interruption until the user resolves it.
  • Sheet — an edge-attached modal panel.