Components / Overlays
Toast
A short message about something that just happened, shown along one edge of the screen and gone after a few seconds: "Saved", "Copied", "Upload failed". It never takes focus and never blocks the page.
When to useLink to "When to use"
- Confirmation of an action the person just took, when the result is not otherwise visible.
- A background event worth knowing about now (an upload failed, sync paused).
Use something else when:
- the message belongs to one part of the page and should stay: a Notice
- the person must decide something: Dialog
- the message explains a form error: the field's own error text
UsageLink to "Usage"
Put one Toaster around the part of the app that raises toasts, then call useToast() anywhere inside it.
import { Button, Toaster, useToast } from "@arshad-shah/cynosure";
<Toaster position="bottom-end">
<App />
</Toaster>
function Archive() {
const { toast } = useToast();
return (
<Button
onClick={() =>
toast({ title: "Archived 3 files", action: { label: "Undo", onClick: restore } })
}
>
Archive
</Button>
);
}toast() returns an id; dismiss(id) closes that toast and dismiss() closes them all. Each Toaster has its own store, so two products on one page (or two extension surfaces) never share toasts.
Loading, then doneLink to "Loading, then done"
Give a toast an id and call toast() again with the same id to update it in place: the same toast, not a second one. loading: true shows a spinner instead of the tone's icon and keeps the toast until it is updated or dismissed; the update's duration starts afresh.
const { toast } = useToast();
toast({ id: "export", title: "Exporting", loading: true });
await exportRows();
toast({ id: "export", title: "Exported 120 rows", tone: "success" });onDismiss(id, reason) is called once when a toast starts to close: "timeout" when its timer ran out, "user" for the close button or a swipe, "programmatic" for dismiss().
StackingLink to "Stacking"
At most max toasts show at once. Older ones fold away and come back as newer ones close; they are the same elements, so a screen reader does not hear them twice.
Keeping clear of fixed UILink to "Keeping clear of fixed UI"
Toasts sit a small margin in from the edges, plus the device's safe area. When the app has something fixed along the edge they sit on, such as a bottom tab bar on a phone, offset moves them further in.
<Toaster position="bottom-center" offset="56px">
<App />
</Toaster>- One value is the distance from the top or bottom edge, whichever
positionnames: a step of the space scale ("8") or any CSS length ("72px","calc(56px + 1rem)"). { block, inline }also sets the distance from the side:offset={{ block: "56px", inline: "4" }}. The side follows the reading direction; a centred stack keeps its place and gives up room on both sides.- To set it from CSS instead, for example only below a breakpoint, use the tokens
--cy-toast-offset-blockand--cy-toast-offset-inlineon any element around the Toaster. The prop wins over them.
@media (max-width: 48rem) {
.app {
--cy-toast-offset-block: 56px;
}
}Inside a DialogLink to "Inside a Dialog"
Toasts raised while a Dialog is open appear above it, and carry on after it closes.
PropsLink to "Props"
Behaviour and accessibilityLink to "Behaviour and accessibility"
| Input | Result |
|---|---|
| Pointer over the toasts | Timers pause until it leaves |
| Focus inside a toast (Tab into it) | Timers pause until focus leaves |
| Close button | Closes that toast |
| Action button | Runs the action; the toast stays until it times out or is closed |
- Announcements: each toast is a live element:
role="status"(polite), orrole="alert"(assertive) fortone="danger". Its text arrives once, after the live element exists, so it is announced once. An update changes the text in the same live element: only a change is announced, and an update with the same text announces nothing. The loading spinner is decorative. - Region: toasts sit in a region named "Notifications" in the browser top layer, so no parent can clip them. Pointer events pass through the empty parts of the region.
- Edges: the region keeps the toasts inside the device's safe area on every side (
env(safe-area-inset-*)), then addsoffset. Its empty parts never take presses, so a tab bar under it still works. - Hidden page: timers also pause while the tab is hidden, so a toast is not missed.
- Modal dialogs: while a Dialog or Sheet inside the Toaster is open, the toasts show inside it, above its backdrop, so they stay visible, clickable and announced (outside, the modal would make them inert). When the dialog closes they move back to the page with the time they had left, and are not announced a second time.
- Motion: toasts slide and fade in and out, and do not move under reduced motion.
TouchLink to "Touch"
Swipe a toast sideways by 64px or more to dismiss it; a shorter swipe springs back. Close and action buttons keep a 44px hit area on coarse pointers.
ThemingLink to "Theming"
| Token | What it sets |
|---|---|
--cy-toast-width | Width of a toast (never wider than the screen) |
--cy-toast-radius | Corner radius |
--cy-toast-padding | Padding inside a toast |
--cy-toast-gap | Space between stacked toasts |
--cy-toast-offset-block | Extra distance from the top or bottom edge the toasts sit on |
--cy-toast-offset-inline | Extra distance from the side they sit on |
createTheme({ components: { toast: { width: "400px", radius: "10px", offsetBlock: "56px" } } });RelatedLink to "Related"
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.