Toast

A short message about something that just happened, which leaves by itself. Toasts live in a store you make with createToaster(), so any code can show one, and appear in one Toast.Region near the root of your app, which takes the theme where it is rendered.

// Once, at module scope.
export const toaster = createToaster();

// Once, near the app root.
<Toast.Region toaster={toaster}>
	{(toast) => (
		<Toast.Root>
			<Toast.Icon>
				<YourIcon />
			</Toast.Icon>
			<Toast.Title>{toast.title}</Toast.Title>
			<Toast.Description>{toast.description}</Toast.Description>
			<Toast.Action onClick={undo}>Undo</Toast.Action>
			<Toast.Close>×</Toast.Close>
		</Toast.Root>
	)}
</Toast.Region>

// Anywhere, even outside React.
toaster.add({ tone: 'success', title: 'Changes saved' });

Toasts over a dialog

Toasts already showing moved in front of this dialog. New ones land there too.

toaster.add() returns the toast's id. Pass that id to update() to change the toast while it shows, such as from "Uploading" to "Uploaded" (give a toast that waits on work duration: Infinity), or to dismiss() to close it. Adding a toast with an id the toaster already has replaces that toast in place. Give createToaster<YourFields>() a type to put fields of your own on each toast and read them in the render function.

Todae ships no icons. Toast.Icon is a slot for one from your own set, as in Alert.

Toasts and modal dialogs

The toasts sit in a popover="manual" element that the region creates, so they are in the top layer above the page. A modal <dialog> is in the top layer too, and it makes everything outside it inert: a toast outside an open dialog can't be clicked or focused, and screen readers can't find it, even when it is drawn on top. Showing the popover again doesn't change that.

So while a Dialog or BottomSheet is open, the region moves inside it and shows itself again. Toasts shown before the dialog opened and toasts shown while it is open both sit in front of the dialog and its backdrop, and stay usable. When the dialog has closed, the region moves back. While a modal is open, the toasts sit inside it, so they take its theme. With nested dialogs, it follows the one in front.

The region knows about a modal from Todae's modal stack. Dialog and BottomSheet join it by themselves. For a modal <dialog> of your own, call useModalLayer. Toasts stay inert under a modal that is not in the stack.

Each new toast shows the region again, so it lands in front of a Popover or Menu opened since the last one. Clicking a toast closes an open popover that closes on outside clicks, as any click outside it does.

Layouts

layout="list" shows each toast in full. layout="stack" puts the toasts behind the newest one, peeking out by toast.peek, and spreads them out while the pointer is over them or keyboard focus is inside. The newest toast sits nearest the edge placement names.

limit caps how many toasts show at once. Toasts added past it wait their turn and show as others leave.

The region sets these variables on each li for your own stack styles: --toast-index (0 for the newest), --toast-offset (the height of the toasts in front of it), and on the list --toast-front-height and --toast-count.

Timing

A toast dismisses itself after duration: 5000 milliseconds unless createToaster() or add() says otherwise. Infinity keeps it until it is dismissed. Every timer pauses while the pointer is over the region, while keyboard focus is inside it, and while the page is hidden.

A toast can leave before someone reaches it, so it should not be the only way to do something. Offer Undo somewhere else too, or give a toast with an action duration: Infinity. Toast.Action dismisses its toast after onClick; call event.preventDefault() to keep it. An action that opens a Dialog leaves it nothing to return focus to once the toast is gone, so pass the Dialog a restoreFocusRef.

Keyboard and screen readers

  • F8 moves focus to the region while a toast shows, and Tab goes on into the toasts. Change it with hotkey, such as hotkey="Alt+T", or turn it off with hotkey={null}.
  • Escape on a toast dismisses it, not the dialog behind it. Focus stays in the region while other toasts remain, and goes back where it came from once none is left.
  • The toasts are a list inside a live region that exists before the first toast, so screen readers read each new one politely, once. That includes critical toasts: a message that must interrupt belongs in an Alert.
  • The region is named "Notifications" and Toast.Close is named "Dismiss" unless its text names it. Both come from the notifications and dismiss strings of StringsProvider, English unless you set them; a non-empty aria-label still wins.

When to use

When: Confirming an action that already happened, such as saved, sent or copied, or a background event that needs no answer.

When not: A message that stays until it is fixed → Alert. An error on a field → ErrorMessage. A choice that blocks the task → Dialog. A toast holds text and buttons, with their tooltips, not a menu, popover or dialog of its own: Escape inside it dismisses the toast.

Props

Toast.Region props
NameTypeRequiredDefaultDescription
toasterToaster<T>yes—The store from createToaster().
children(toast: ToastRecord<T>) => React.ReactNodeyes—Renders one toast, usually a Toast.Root.
placement"top-start" | "top-center" | "top-end" | "bottom-start" | "bottom-center" | "bottom-end"no"bottom-end"Corner or edge of the viewport. The newest toast sits nearest the edge.
layout"list" | "stack"no"list"list shows every toast in full. stack collapses them behind the newest and spreads them while the pointer is over them or keyboard focus is inside.
limitnumberno3How many toasts show at once. Later ones wait their turn.
hotkeystring | nullno"F8"Key that moves focus to the toasts, such as "F8" or "Alt+T". Modifiers are Alt (or Option), Ctrl, Meta (or Cmd) and Shift. null turns it off.
aria-labelstringno"Notifications"Name of the region landmark. Default is the notifications string from StringsProvider.
Toast.Root props
NameTypeRequiredDefaultDescription
tone"info" | "success" | "warning" | "critical"nothe toast's own toneColors from color.status.<tone> through the toast contract tokens.
childrenReact.ReactNodeno—Toast parts, or any content.
Toast.Icon props
NameTypeRequiredDefaultDescription
childrenReact.ReactNodeno—Icon graphic from your own icon set.
Toast.Title props
NameTypeRequiredDefaultDescription
childrenReact.ReactNodeno—Title text.
Toast.Description props
NameTypeRequiredDefaultDescription
childrenReact.ReactNodeno—Description content.
Toast.Action props
NameTypeRequiredDefaultDescription
variant"primary" | "secondary" | "ghost"no"secondary"Button variant.
onClickReact.MouseEventHandler<HTMLButtonElement>no—Runs the action. Call event.preventDefault() to keep the toast open.
Toast.Close props
NameTypeRequiredDefaultDescription
aria-labelstringno"Dismiss" when its text, not counting aria-hidden or SVG parts, has fewer than two letters or digitsAccessible name. Text of two or more letters or digits names the button by itself. Otherwise the dismiss string from StringsProvider names it, Dismiss in English.
onClickReact.MouseEventHandler<HTMLButtonElement>no—Runs before the toast is dismissed. Call event.preventDefault() to keep it open.
createToaster props
NameTypeRequiredDefaultDescription
durationnumberno5000Default milliseconds before a toast dismisses itself. Infinity keeps toasts until dismissed.