Skip to content

Toast

Brief feedback after an action, with undo for anything reversible.

Status
Stable
Category
Feedback
Adoption
Not used yet
import { toast } from "@oration/canon/components/toast";
packages/canon/src/components/toast.tsx

Payment run, Friday, Oct 2

3 invoices

  • INV-20931Northwind Freight$18,420.00
  • INV-20944Halcyon$6,112.50
  • INV-20958Orchard Street$2,980.00
import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function Hero() {    const [invoices, setInvoices] = React.useState([        {            id: "INV-20931",            supplier: "Northwind Freight",            amount: "$18,420.00",        },        { id: "INV-20944", supplier: "Halcyon", amount: "$6,112.50" },        { id: "INV-20958", supplier: "Orchard Street", amount: "$2,980.00" },    ]);    const remove = (id: string) => {        const before = invoices;        const target = invoices.find((invoice) => invoice.id === id);        if (!target) return;        setInvoices((current) =>            current.filter((invoice) => invoice.id !== id),        );        toast.add({            title: `Removed ${target.id} from the run`,            description: `${target.supplier} is paid in the next run instead.`,            actionProps: {                children: "Undo",                onClick: () => setInvoices(before),            },        });    };    return (        <div className="w-full max-w-md rounded-xl bg-card text-left shadow-border">            <div className="flex items-baseline justify-between px-4 pt-3.5 pb-1.5">                <p className="text-sm font-semibold">                    Payment run, Friday, Oct 2                </p>                <p className="text-xs text-muted-foreground tabular-nums">                    {invoices.length} invoices                </p>            </div>            <ul className="flex flex-col px-2 pb-2">                {invoices.map((invoice) => (                    <li                        key={invoice.id}                        className="flex h-10 items-center gap-3 rounded-lg px-2 text-13"                    >                        <span className="font-mono text-xs text-muted-foreground">                            {invoice.id}                        </span>                        <span className="min-w-0 flex-1 truncate font-medium">                            {invoice.supplier}                        </span>                        <span className="tabular-nums">{invoice.amount}</span>                        <Button                            type="button"                            variant="ghost"                            size="xs"                            onClick={() => remove(invoice.id)}                        >                            Remove                        </Button>                    </li>                ))}                {invoices.length === 0 ? (                    <li className="px-2 py-2.5 text-13 text-muted-foreground">                        Every invoice was removed from this run.                    </li>                ) : null}            </ul>        </div>    );}

Usage#

Toast is the brief answer to an action: Remittance sent, Removed INV-20931 from the run, Couldn't send the remittance. Call toast.add() from any client component; the one Toaster is mounted in the root layout, so never mount another. Toasts stack at the bottom right, pause while hovered or focused, and leave after five seconds. The common mistake is asking for confirmation before a reversible change; do the change, then offer Undo in the toast.

When to use

  • To confirm a change whose result isn't visible where the person is looking: Remittance sent, Invite sent to Wen Zhou.
  • To offer Undo after a reversible change: removing an invoice from a run, archiving a supplier, denying a request.
  • To report that a background action failed, with Retry as the action.
  • To show a short async task from start to finish with toast.promise: Sending remittances, then Sent 48 remittances.
  • To summarize a bulk action in one line: Approved 12 invoices.

When not to use

  • For a condition that must stay visible until someone fixes it, such as a rejected W-9. Use Alert
  • For an irreversible delete that needs a decision first. Use Confirm dialog
  • For a validation error on a field. Put it under the field, where it can be fixed. Use Field
  • For unsaved changes. The save bar holds them until they're saved or discarded. Use Save bar
  • To confirm a copy. The copy button confirms in place with a check. Use Copy button
  • When the change is already visible in place, such as a switch flipping or a row updating. A toast on top is noise.

Undo over confirm

Reversible changes happen at once and offer Undo in the toast. A confirmation dialog is only for changes that can't be undone. See Destructive actions.

Every mutation answers back

A save, send, archive or delete always gets feedback: inline where it changed, or in a toast when the result is out of view. See Feedback and undo.

Anatomy#

  1. Container. Popover White with 16px corners, a 1px border and the dialog shadow. Up to 24rem wide at the bottom right, full width with 16px margins on phones.
  2. Icon. Drawn from type: a check for success, an i for info, a triangle for warning, a red octagon for error and a spinner for loading. No icon without a type.
  3. Title. 14px at weight 500. The outcome, in a few words.
  4. Description. Optional. 14px Slate Meta: the object and the detail that matters, such as the supplier and amount.
  5. Action. Optional, from actionProps. A small outline button, usually Undo or Retry. Clicking it runs your handler and closes the toast.
  6. Close. A small ghost icon button named Close toast, with its hit area extended 8px on each side.

Examples#

Types

type picks the icon: success, info, warning, error, or none. The title carries the meaning; the icon only confirms it.

import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function Types() {    return (        <>            <Button                type="button"                variant="outline"                onClick={() =>                    toast.add({                        type: "success",                        title: "Remittance sent",                        description: "Northwind Freight, $18,420.00 by ACH.",                    })                }            >                Success            </Button>            <Button                type="button"                variant="outline"                onClick={() =>                    toast.add({                        type: "info",                        title: "Halcyon updated their bank details",                        description:                            "Priya Raman verifies them before the next run.",                    })                }            >                Info            </Button>            <Button                type="button"                variant="outline"                onClick={() =>                    toast.add({                        type: "warning",                        title: "2 invoices skipped",                        description:                            "Their PO numbers don't match. They wait for review.",                    })                }            >                Warning            </Button>            <Button                type="button"                variant="outline"                onClick={() =>                    toast.add({                        type: "error",                        title: "Couldn't send the remittance",                        description:                            "The bank rejected the file. Try again in a minute.",                    })                }            >                Error            </Button>            <Button                type="button"                variant="outline"                onClick={() => toast.add({ title: "Copied INV-20931" })}            >                No type            </Button>        </>    );}

Undo and Retry

actionProps adds one button. Undo reverses a change that already happened; Retry runs a failed action again. Both close the toast when clicked.

Orchard StreetActive
import { Button } from "@oration/canon/components/button";import { StatusLabel } from "@oration/canon/components/status-dot";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function WithActions() {    const [archived, setArchived] = React.useState(false);    const archive = () => {        setArchived(true);        toast.add({            title: "Archived Orchard Street",            description: "Their open invoices stay in Friday's run.",            actionProps: {                children: "Undo",                onClick: () => setArchived(false),            },        });    };    const retry = () => {        toast.add({            type: "error",            title: "Couldn't sync Halcyon's invoices",            description: "The ERP didn't respond in time.",            actionProps: {                children: "Retry",                onClick: () =>                    toast.add({                        type: "success",                        title: "Synced 14 invoices from Halcyon",                    }),            },        });    };    return (        <div className="flex flex-wrap items-center gap-3">            <div className="flex h-10 items-center gap-3 rounded-lg bg-muted/70 px-3 text-13">                <span className="font-medium">Orchard Street</span>                {archived ? (                    <StatusLabel tone="neutral">Archived</StatusLabel>                ) : (                    <StatusLabel tone="success">Active</StatusLabel>                )}                <Button                    type="button"                    variant="outline"                    size="xs"                    disabled={archived}                    onClick={archive}                >                    Archive                </Button>            </div>            <Button type="button" variant="outline" onClick={retry}>                Sync Halcyon            </Button>        </div>    );}

Loading to done

toast.promise shows a loading toast with a spinner and turns it into the success or error toast in place when the promise settles.

import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function Pending() {    const send = () => {        toast.promise(            new Promise<number>((resolve) => {                window.setTimeout(() => resolve(48), 1800);            }),            {                loading: {                    title: "Sending remittances",                    description: "48 suppliers in the Oct 2 run.",                },                success: (count) => ({                    title: `Sent ${count} remittances`,                    description:                        "Suppliers get them by email within a few minutes.",                }),                error: {                    title: "Couldn't send remittances",                    description: "Nothing was sent. Try again in a minute.",                },            },        );    };    return (        <Button type="button" variant="outline" onClick={send}>            Send remittances        </Button>    );}

Update in place

Pass an id for feedback that repeats. Press Save draft a few times: one toast updates and its timer restarts, instead of a stack.

import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function UpdateInPlace() {    const save = () => {        const time = new Date().toLocaleTimeString("en-US", {            hour: "numeric",            minute: "2-digit",            second: "2-digit",        });        toast.add({            id: "payment-terms-draft",            title: "Draft saved",            description: `Northwind Freight payment terms, saved at ${time}.`,        });    };    return (        <Button type="button" variant="outline" onClick={save}>            Save draft        </Button>    );}

Stays until dismissed

timeout: 0 keeps a toast open. Reserve it for a failure the person has to act on; everything else leaves after five seconds.

import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function Persistent() {    return (        <Button            type="button"            variant="outline"            onClick={() =>                toast.add({                    type: "error",                    title: "Payment to Halcyon failed",                    description:                        "The account was closed. Update their bank details.",                    timeout: 0,                    actionProps: {                        children: "Update",                        onClick: () =>                            toast.add({                                title: "Opened Halcyon's bank details",                            }),                    },                })            }        >            Run payment        </Button>    );}

States#

States
StateTreatment
EnteringSlides up from 150% below the viewport edge over 500ms on cubic-bezier(0.22, 1, 0.36, 1).
StackedOlder toasts sit behind the newest, each 12px higher and 10% smaller, with their content faded out. Only the front toast reads.
ExpandedHovering or focusing the stack fans the toasts out 12px apart at full size and pauses every timer.
Focus visibleA Focus Indigo border and a 3px ring at 50% on the toast; its action and close button take the button focus ring.
LimitedPast three toasts, the oldest fade to transparent and leave the tab order until newer ones close.
Loadingtype: "loading", set by toast.promise, shows a spinner. It updates to success or error in place when the promise settles.
SwipingDragging down or right follows the pointer; past the threshold the toast leaves in that direction.
ExitingSlides back down 150% (or out the way it was swiped) over the same 500ms.

Behavior#

  • toast is a module-level Base UI toast manager, so toast.add() works from any client component, event handler or store without a hook or context.
  • toast.add() returns the toast's id. Pass your own id to update a toast in place instead of stacking a new one; adding with an existing id refreshes its timer.
  • Toasts close after 5000ms. timeout: 0 keeps one open until it's dismissed; use it for an error that needs a decision.
  • Timers pause while the stack is hovered or focused and while the window is in the background, so an Undo is always reachable.
  • actionProps is spread onto the action button. { children: "Undo", onClick } is the whole pattern; the toast closes after onClick runs.
  • toast.promise(promise, { loading, success, error }) shows a loading toast and turns it into the success or error toast when the promise settles. success and error can be functions of the result.
  • toast.close(id) closes one toast, toast.close() closes all, and toast.update(id, options) changes one in place.
  • At most three toasts show at once; older ones are marked data-limited and hidden until there's room.

Do and don't#

Do. Summarize a bulk action in one toast, with one Undo for all of it.
Don't. Fire a toast per item. Three at once stack into noise and the first ones are hidden.
Do. Name what changed and offer Undo when the change can be reversed.
Don't. Celebrate with Success! and give no way back. The reader can't tell which invoice, or undo it.

Content#

  • Title: the outcome in past tense, with the object: Remittance sent, Removed INV-20931 from the run, Archived Orchard Street.
  • Errors say what failed without blame: Couldn't send the remittance. The description says what to do: Try again in a minute.
  • Description: the detail that makes it specific, such as the supplier, amount or date: Northwind Freight, $18,420.00 by ACH.
  • Loading titles use a present participle and no ellipsis in the string: Sending remittances.
  • The action is one verb: Undo, Retry, View. Never OK or Dismiss; the close button already dismisses.
  • No exclamation points, no Success!, no emoji. Count in figures: Approved 12 invoices.

Accessibility#

  • The viewport is a role="region" named Notifications and an aria-live="polite" region, so new toasts are announced without interrupting.
  • Each toast is a non-modal role="dialog" labelled by its title and described by its description. priority: "high" makes it an alertdialog and announces it urgently; save that for failures that block work.
  • F6 moves focus to the notifications region from anywhere, and Shift+Tab from there returns focus to where it was.
  • The type icon is aria-hidden; the title carries the meaning, so an error reads as an error without the red octagon.
  • Timers pause while a toast has focus or hover, so keyboard and screen reader users can reach Undo before it leaves.
  • An Undo in a toast is a convenience, not the only path. The change should also be reversible from the place it was made.
  • The close button is 28px with an extended hit area, above the 24px minimum.
Keyboard interactions
KeysAction
F6Moves focus to the notifications region and pauses timers.
TabMoves through toasts and their action and close buttons.
ShiftTabFrom the region itself, returns focus to the element that had it before F6.
EnterActivates the focused action or close button.
EscCloses the toast that has focus.

Design tokens#

Design tokens
TokenUsed for
--popoverToast background
--popover-foregroundTitle and icon
--muted-foregroundDescription and close icon
--borderThe 1px toast border
shadow-lgThe dialog shadow under each toast
--destructiveThe error icon
--ringFocus border and 3px ring at 50%
--radius-2xl16px toast corners

API reference#

toast

The app-wide toast manager (Base UI createToastManager()). The Toaster in the root layout listens to it.

Props of toast
PropTypeDefaultDescription
add(options: ToastManagerAddOptions) => stringNo defaultShows a toast and returns its id.
close(id?: string) => voidNo defaultCloses one toast, or every toast when called with no id.
update(id: string, options: ToastManagerUpdateOptions | (prev) => ToastManagerUpdateOptions) => voidNo defaultChanges an open toast in place.
promise<Value>(promise: Promise<Value>, options: { loading, success, error }) => Promise<Value>No defaultShows a loading toast, then the success or error toast when the promise settles. Returns the same promise.

ToastManagerAddOptions

What toast.add() accepts.

Props of ToastManagerAddOptions
PropTypeDefaultDescription
titleReactNodeNo defaultThe outcome. Required in practice.
descriptionReactNodeNo defaultThe object and the detail that matters.
typestringNo defaultPicks the icon: "success", "info", "warning", "error" or "loading". Any other value shows no icon.
actionPropsComponentPropsWithoutRef<"button">No defaultProps for the action button, such as { children: "Undo", onClick }. No action renders without children.
timeoutnumber5000Milliseconds before it closes. 0 keeps it open.
priority"low" | "high""low"high announces urgently and renders as an alertdialog.
idstringNo defaultReuse an id to update a toast in place instead of adding another.
onClose() => voidNo defaultCalled when the toast starts closing.
onRemove() => voidNo defaultCalled after its exit animation finishes.
dataobjectNo defaultCustom data for a custom toast list.

Toaster

Provider, portal, viewport and the default toast list in one. Mounted once in app/layout.tsx; don't add another.

Other props spread onto Base UI Toast.Provider.

Props of Toaster
PropTypeDefaultDescription
toastManagerToastManagertoastThe manager to listen to.
timeoutnumber5000Default time before a toast closes.
limitnumber3How many toasts show at once.
childrenReactNodeNo defaultOptional. Only children can call useToastManager.

useToastManager

Returns { toasts, add, close, update, promise } from the nearest provider, for building a custom toast list. Throws outside Toast.Provider.

No props of its own.

Toast

Toast and its siblings ToastProvider, ToastPortal, ToastViewport, ToastContent, ToastTitle, ToastDescription, ToastAction and ToastClose are styled Base UI parts for building a custom list. createToastManager makes a separate manager. Product code uses toast instead.

Other props spread onto The matching Base UI Toast.* part.

No props of its own.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

Toasts have 16px corners (rounded-2xl), outside the 6, 8, 10 and 12px radius ramp. DESIGN.md puts overlays at 10px and dialogs at 12px.

Each toast draws a 1px CSS border plus shadow-lg, where the Hairline-and-Lift Rule gives overlays one composite shadow with a ring instead.

Enter, exit and stacking run 500ms transforms on cubic-bezier(0.22, 1, 0.36, 1), not the spring tokens, past the 320ms ceiling in the Three Springs Rule. The classes have no motion-reduce variants, so the slide still runs under reduced motion, and the loading spinner keeps spinning.

type is a plain string. The suite brief lists success, error and info; the component also draws warning and loading, and a misspelled type silently shows no icon.

The info icon is drawn in the text color. DESIGN.md names Note Blue for informational toasts.

useToastManager is exported, but the root layout mounts <Toaster /> as a sibling of the page rather than around it, so calling the hook in app code throws. Use toast directly.