Toast
betaA short note in the corner after something happened. An undo toast offers Undo for 8 seconds, then commits the change. Tones say how it went, and a promise toast follows work to its end.
Preview
undo
Design review
Team lunch
Quarterly review
promise
show
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/toast
Use it for
- Place one <Toaster /> near the end of the root layout. Then call toast() from any client code, like an event handler.
- Apply a change that can be undone at once, then call toast.undoable() with onUndo to put it back. onCommit runs when the 8 seconds end.
- Use toast.promise() for work that lands somewhere else, like an import or a send. It shows a spinner, then success or danger.
- Use toast() for news that nothing on screen shows, like a reminder that went out. Give it one action that goes to where the news lives.
- Say what happened in the title, in a few words, like 'Archived Design review'. Add a description only when the title needs it.
- Use tone 'danger' and timeout: 0 for a failure people must see, like a send that did not go, and give it a Retry action.
Avoid it for
- Asking before an action that can be undone. Do it, and offer Undo in a toast.
- An action that cannot be undone, like a payment or a removal. Use Alert Dialog.
- A problem with one field or row. Say it under the thing with Notice.
- A success toast for what the screen already shows, like a saved field. Show the result in place.
- More than one Toaster on a page. Each one draws every toast, so each would show twice.
- Rendering <Toast> to pop a toast. It is a still picture for docs. Call toast().
Props
- Toaster
Where toasts show, in the bottom right corner. Place one on the page.
{ limit?, timeout?, className?, aria-label? }
- limit
Toaster. How many toasts show at once. A newer one replaces the oldest.
number · default 1
- timeout
How long a toast stays, in milliseconds: on Toaster for every toast, or in toast() for one. 0 keeps it until it is dismissed. An undo toast always stays 8 seconds.
number · default 5000
- className
Toaster. Classes for the corner the toasts sit in, like 'right-auto left-4'. Later classes win.
string
- toast
Shows a toast and returns its id. Pressing the action closes the toast, then runs onClick. Showing again with the same id replaces it in place.
(title | { title, description?, tone?, action?: { label, onClick }, timeout?, priority?, id?, onClose? }) => string
- tone
In toast(). Colors and picks the icon. Neutral has none.
"neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"
- priority
In toast(). 'high' reads it out at once. Default 'high' for danger and 'low' for the rest.
"low" | "high"
- toast.promise
Shows loading with a spinner, then success or danger. Each is a title or toast options, and success and error can be functions of the result. Returns the same promise, which still rejects.
(work, { loading, success, error }) => Promise
- toast.undoable
Offers Undo for 8 seconds. Undo or ⌘Z runs onUndo once. Dismissing it, or the window ending, runs onCommit once. With no Toaster on the page, it commits at once.
({ title, description?, onUndo, onCommit?, undoLabel?, priority? }) => string
- toast.close
Closes one toast by id, or every toast with no id.
(id?) => void
- UNDO_WINDOW_MS
How long an undo toast stays, in milliseconds.
8000
- Toast
A still picture of a toast, for docs and galleries. It takes no clicks, and a server file can render it. action is the button's text.
{ title?, description?, tone?, loading?, action? }
- toastVariants
The toast's look. toast.tsx is a client file, so call it from a client file.
({ tone? }) => string
| Prop | Type | Default | What it does |
|---|---|---|---|
| Toaster | { limit?, timeout?, className?, aria-label? } | - | Where toasts show, in the bottom right corner. Place one on the page. |
| limit | number | 1 | Toaster. How many toasts show at once. A newer one replaces the oldest. |
| timeout | number | 5000 | How long a toast stays, in milliseconds: on Toaster for every toast, or in toast() for one. 0 keeps it until it is dismissed. An undo toast always stays 8 seconds. |
| className | string | - | Toaster. Classes for the corner the toasts sit in, like 'right-auto left-4'. Later classes win. |
| toast | (title | { title, description?, tone?, action?: { label, onClick }, timeout?, priority?, id?, onClose? }) => string | - | Shows a toast and returns its id. Pressing the action closes the toast, then runs onClick. Showing again with the same id replaces it in place. |
| tone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | In toast(). Colors and picks the icon. Neutral has none. |
| priority | "low" | "high" | - | In toast(). 'high' reads it out at once. Default 'high' for danger and 'low' for the rest. |
| toast.promise | (work, { loading, success, error }) => Promise | - | Shows loading with a spinner, then success or danger. Each is a title or toast options, and success and error can be functions of the result. Returns the same promise, which still rejects. |
| toast.undoable | ({ title, description?, onUndo, onCommit?, undoLabel?, priority? }) => string | - | Offers Undo for 8 seconds. Undo or ⌘Z runs onUndo once. Dismissing it, or the window ending, runs onCommit once. With no Toaster on the page, it commits at once. |
| toast.close | (id?) => void | - | Closes one toast by id, or every toast with no id. |
| UNDO_WINDOW_MS | 8000 | - | How long an undo toast stays, in milliseconds. |
| Toast | { title?, description?, tone?, loading?, action? } | - | A still picture of a toast, for docs and galleries. It takes no clicks, and a server file can render it. action is the button's text. |
| toastVariants | ({ tone? }) => string | - | The toast's look. toast.tsx is a client file, so call it from a client file. |
Accessibility
- Toasts are read out by a live region. The default priority waits until the screen reader is quiet. 'high', the default for danger, reads at once.
- Each tone has its own icon, so the tone does not rest on color alone. A loading toast sets aria-busy.
- F6 moves focus to the toast, so keyboard users can reach its action and Dismiss.
- The window holds while the pointer is on the toast, and while focus is in it.
- ⌘Z or Ctrl+Z undoes the newest change while its window is open. It leaves text fields alone, so their own undo still works.
- The Dismiss button is named for screen readers. It stays hidden from them until the toast is pointed at or focused.
- The toast uses the popover colors. The theme tests hold its muted text and each tone's icon color to 4.5:1 on popovers.
Examples
Archive at once, and offer Undo
function EventRow({ id, title }: { id: string; title: string }) {
const [hidden, setHidden] = useState(false);
if (hidden) return null;
return (
<div className="flex items-center justify-between gap-3">
{title}
<Button
emphasis="ghost"
onClick={() => {
setHidden(true);
toast.undoable({
title: `Archived ${title}`,
onUndo: () => setHidden(false),
onCommit: () => fetch(`/api/events/${id}/archive`, { method: "POST" }),
});
}}
>
Archive
</Button>
</div>
);
}Place one Toaster in the root layout
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
);
}Follow an import to its end
toast.promise(importMembers(file), {
loading: "Importing members…",
success: (added) => ({
title: "Import finished",
description: `${added} members added to the workspace.`,
action: { label: "Open", onClick: () => router.push("/members") },
}),
error: "Could not import",
});A failure that stays until it is dismissed
toast({
title: "Reminder not sent",
description: "Check your connection, then try again.",
tone: "danger",
action: { label: "Retry", onClick: sendReminder },
timeout: 0,
});