A brief corner notice of an event, with four tones, an optional action and a polite announcement.
Tones
A toast reports an event that just happened, such as a save. It shows in the bottom end corner, 16px in, above every dialog.
- Tone: a 4px stripe at its start, in slate for
info, or insuccess,warningorerror. - Text: a title says what happened, and an optional second line in slate says more.
- Timing: it leaves after 6 seconds, or 12 for an error. Its X dismisses it sooner.
- Stacking: the newest sits nearest the corner. Three show at once, and a fourth hides the oldest until one leaves.
Example
Example
'use client';
import { Button, toast, type ToastOptions } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
const EVENTS: ToastOptions[] = [
{ title: 'Saved', description: 'Your changes are live.', tone: 'success' },
{ title: 'Sync started', description: 'New transactions show up in a minute or two.', tone: 'info' },
{
title: 'Low on storage',
description: 'The workspace has used 9.2 of its 10 GB. Archive old files or add room in Settings.',
tone: 'warning',
},
{
title: 'Import failed',
description: 'The file is not a CSV. Choose another one and try again.',
tone: 'error',
},
];
export default function ToastBasic(): ReactElement {
return (
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 16 }}>
{EVENTS.map((options) => (
<Button
key={options.tone}
variant="secondary"
onPress={() => {
toast(options);
}}
>
{options.title}
</Button>
))}
</div>
);
}With an action
A toast can offer one action, such as Undo, as a secondary button before its X. A toast with an action stays until the action is used or the toast is dismissed, so the reader has time to reach it. The action runs, then the toast closes.
Example
Example
- Checking
- Savings
- Credit card
'use client';
import { Button, toast } from '@snapshot-ui/react';
import { useEffect, useRef, useState, type ReactElement } from 'react';
const ACCOUNTS = ['Checking', 'Savings', 'Credit card'];
export default function ToastAction(): ReactElement {
const [rows, setRows] = useState(ACCOUNTS);
// Each row's Remove button, so focus can be put on a row that stays.
const buttons = useRef(new Map<string, HTMLButtonElement>());
const restore = useRef<HTMLButtonElement>(null);
// The row to focus once the render that changes the list is done, or null for the button that restores all.
const next = useRef<string | null | undefined>(undefined);
// The toast that offers the last Undo: one at a time, so a newer removal replaces the older offer.
const offer = useRef<string | null>(null);
useEffect(() => {
const name = next.current;
if (name === undefined) return;
next.current = undefined;
(name === null ? restore.current : buttons.current.get(name))?.focus();
}, [rows]);
const remove = (name: string): void => {
const index = rows.indexOf(name);
const rest = rows.filter((row) => row !== name);
next.current = rest[index] ?? rest[index - 1] ?? null;
setRows(rest);
if (offer.current !== null) toast.dismiss(offer.current);
const id = toast({
title: `${name} removed`,
description: 'Its transactions stay in your reports.',
tone: 'success',
// An action keeps the toast until it is used or dismissed.
action: {
label: 'Undo',
onAction: () => {
next.current = name;
setRows((current) => ACCOUNTS.filter((row) => row === name || current.includes(row)));
},
},
onClose: () => {
if (offer.current === id) offer.current = null;
},
});
offer.current = id;
};
return (
<div style={{ width: '100%', maxWidth: '24rem' }}>
{rows.length > 0 ? (
<ul
aria-label="Accounts"
style={{ display: 'grid', gap: 8, margin: 0, padding: 0, listStyle: 'none' }}
>
{rows.map((row) => (
<li
key={row}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}
>
<span>{row}</span>
<Button
variant="secondary"
aria-label={`Remove ${row}`}
ref={(element) => {
if (element) buttons.current.set(row, element);
else buttons.current.delete(row);
}}
onPress={() => {
remove(row);
}}
>
Remove
</Button>
</li>
))}
</ul>
) : (
<Button
ref={restore}
variant="secondary"
onPress={() => {
// Everything is back, so the last Undo has nothing left to offer.
if (offer.current !== null) toast.dismiss(offer.current);
setRows(ACCOUNTS);
}}
>
Put them back
</Button>
)}
</div>
);
}Usage
Mount <ToastRegion /> once at the app root, beside your client providers, as this site does in its docs layout. Then call toast() from any client code: a press handler, an effect, or the line after a server action returns. tone is required. The title and second line are text that a status region reads out, so never put a link or button in them. Use action for the toast's one control.
'use client';
import { Button, toast } from '@snapshot-ui/react';
import { saveBudget } from './actions';
export function SaveBudget({ budget }: { budget: Budget }) {
return (
<Button
onPress={async () => {
// A new save starts, so the last result is stale.
toast.clear();
const result = await saveBudget(budget);
toast(
result.ok
? { title: 'Saved', tone: 'success' }
: { title: 'The budget was not saved', description: result.message, tone: 'error' },
);
}}
>
Save
</Button>
);
}Toasts follow the page's theme and corners, not the scope around the button that raised them, since a toast reports on the app. See Theming.
toast()returns the toast's id, andtoast.dismiss(id)closes that toast.toast.clear()drops every toast, errors included. Call it when a new save starts, since the last result is stale.timeoutsets milliseconds, ornull(or zero) to stay until dismissed. The defaults are 6 seconds, 12 for an error, and no timeout with an action.- Hovering or focusing the toasts pauses every timer, including for a toast that arrives meanwhile. The timers restart when both pointer and focus leave.
- With no region mounted, a toast shows nothing and the console warns once. A second region also warns, since it would show and announce every toast twice.
Say the outcome in the title, in words. The stripe only repeats it, and in Windows high contrast it is repainted in the text color. Keep one toast with an action at a time: since three show at once and action toasts stay, extra ones push the oldest Undo out of sight. When a newer action replaces an older one, dismiss the older one, as the example above does.
Props
toast() takes these options. ToastRegion places and announces the toasts, and its className goes on the region, after the library's classes.
action ToastAction | A button such as "Undo". A toast with an action stays until it is used or dismissed. |
|---|---|
description ReactNode | A second line, in slate. Text only, as |
onClose (() => void) | Called when it leaves, however it leaves. |
timeout number | null | Milliseconds before it leaves, or |
title ReactNode | The message. Say the outcome in words, since the tone only repeats it. Text only: a status region reads it
out, so put a button in |
tone ToastTone |
aria-label string | The region's name. Default: React Aria's localized count, such as "2 notifications." |
|---|---|
className string | Goes on the region, after its classes. |
dismissLabel string | The dismiss button's accessible name. Default: 'Dismiss' |
maxVisibleToasts number | How many toasts show at once. The newest always shows, and the oldest is hidden until one leaves. Applies from the next toast. Default: 3 |
Accessibility
The toasts sit in a landmark region that F6 reaches from anywhere. It is named by React Aria's localized count of notifications unless aria-label names it. Each toast is a non-modal alert dialog, named by its title and described by its second line.
- An error is announced assertively, as an alert.
- Other tones are announced politely through a status region that
ToastRegionrenders before any toast. Each toast is announced once, even when its words repeat. A screen reader only reliably announces a live region that was already there. - Toasts stay pressable and announced while a dialog is open, and pressing one never closes the dialog.
- When a focused toast leaves, focus moves to the next toast, or back to where it was.
- With reduced motion, a toast appears without its rise.
| F6 | Reaches the toast region. |
|---|---|
| Tab | Moves through a toast, its action and its dismiss. |
| EnterSpace | Press the action or the dismiss. |
Design rules
What keeps toasts in the system.
Say the outcome in words, such as Saved or The import failed.
Keep one toast with an action at a time.
Clear the toasts when a new save starts.
Never use a toast for a state a form should show, such as three fields that are invalid.
Never let color alone carry the tone.
Never use a toast for something that needs a decision. That is a confirm dialog.