Toast
Brief feedback after an action, with undo for anything reversible.
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
Every mutation answers back
Anatomy#
- 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.
- 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. - Title. 14px at weight 500. The outcome, in a few words.
- Description. Optional. 14px Slate Meta: the object and the detail that matters, such as the supplier and amount.
- Action. Optional, from
actionProps. A small outline button, usually Undo or Retry. Clicking it runs your handler and closes the toast. - 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.
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#
| State | Treatment |
|---|---|
| Entering | Slides up from 150% below the viewport edge over 500ms on cubic-bezier(0.22, 1, 0.36, 1). |
| Stacked | Older toasts sit behind the newest, each 12px higher and 10% smaller, with their content faded out. Only the front toast reads. |
| Expanded | Hovering or focusing the stack fans the toasts out 12px apart at full size and pauses every timer. |
| Focus visible | A Focus Indigo border and a 3px ring at 50% on the toast; its action and close button take the button focus ring. |
| Limited | Past three toasts, the oldest fade to transparent and leave the tab order until newer ones close. |
| Loading | type: "loading", set by toast.promise, shows a spinner. It updates to success or error in place when the promise settles. |
| Swiping | Dragging down or right follows the pointer; past the threshold the toast leaves in that direction. |
| Exiting | Slides back down 150% (or out the way it was swiped) over the same 500ms. |
Behavior#
toastis a module-level Base UI toast manager, sotoast.add()works from any client component, event handler or store without a hook or context.toast.add()returns the toast's id. Pass your ownidto update a toast in place instead of stacking a new one; adding with an existing id refreshes its timer.- Toasts close after 5000ms.
timeout: 0keeps 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.
actionPropsis spread onto the action button.{ children: "Undo", onClick }is the whole pattern; the toast closes afteronClickruns.toast.promise(promise, { loading, success, error })shows a loading toast and turns it into the success or error toast when the promise settles.successanderrorcan be functions of the result.toast.close(id)closes one toast,toast.close()closes all, andtoast.update(id, options)changes one in place.- At most three toasts show at once; older ones are marked
data-limitedand hidden until there's room.
Do and don't#
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 anaria-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 analertdialogand 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.
| Keys | Action |
|---|---|
| F6 | Moves focus to the notifications region and pauses timers. |
| Tab | Moves through toasts and their action and close buttons. |
| ShiftTab | From the region itself, returns focus to the element that had it before F6. |
| Enter | Activates the focused action or close button. |
| Esc | Closes the toast that has focus. |
Design tokens#
| Token | Used for |
|---|---|
--popover | Toast background |
--popover-foreground | Title and icon |
--muted-foreground | Description and close icon |
--border | The 1px toast border |
shadow-lg | The dialog shadow under each toast |
--destructive | The error icon |
--ring | Focus border and 3px ring at 50% |
--radius-2xl | 16px toast corners |
API reference#
toast
The app-wide toast manager (Base UI createToastManager()). The Toaster in the root layout listens to it.
| Prop | Type | Default | Description |
|---|---|---|---|
add | (options: ToastManagerAddOptions) => string | No default | Shows a toast and returns its id. |
close | (id?: string) => void | No default | Closes one toast, or every toast when called with no id. |
update | (id: string, options: ToastManagerUpdateOptions | (prev) => ToastManagerUpdateOptions) => void | No default | Changes an open toast in place. |
promise | <Value>(promise: Promise<Value>, options: { loading, success, error }) => Promise<Value> | No default | Shows a loading toast, then the success or error toast when the promise settles. Returns the same promise. |
ToastManagerAddOptions
What toast.add() accepts.
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | No default | The outcome. Required in practice. |
description | ReactNode | No default | The object and the detail that matters. |
type | string | No default | Picks the icon: "success", "info", "warning", "error" or "loading". Any other value shows no icon. |
actionProps | ComponentPropsWithoutRef<"button"> | No default | Props for the action button, such as { children: "Undo", onClick }. No action renders without children. |
timeout | number | 5000 | Milliseconds before it closes. 0 keeps it open. |
priority | "low" | "high" | "low" | high announces urgently and renders as an alertdialog. |
id | string | No default | Reuse an id to update a toast in place instead of adding another. |
onClose | () => void | No default | Called when the toast starts closing. |
onRemove | () => void | No default | Called after its exit animation finishes. |
data | object | No default | Custom 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.
| Prop | Type | Default | Description |
|---|---|---|---|
toastManager | ToastManager | toast | The manager to listen to. |
timeout | number | 5000 | Default time before a toast closes. |
limit | number | 3 | How many toasts show at once. |
children | ReactNode | No default | Optional. 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.