Skip to content

Toast

  • Confirming an action: “Saved”, “Message sent”
  • Offering a quick Undo after an action
  • Background events that need no decision: “Connection restored”

Not for errors that need attention or information that must stay — use an Alert or a Dialog.

Choosing between messages and popups:

ComponentUse it forFor example
DialogSomething that needs a decision or input before the user carries on; the page is blocked meanwhileConfirm a delete, edit a record in a form
DrawerA panel that slides in from an edge and blocks the page; the first choice on a phoneShare options, a mobile nav, a filter panel
PopoverExtra content or controls next to a button, while the rest of the page stays usableFilter panel, share options, details card
MenuA list of actions or settings opened from a button“More actions ⋯”, a View menu
TooltipA short text label on hover or focus; nothing in it can be clickedName of an icon button, a keyboard shortcut
HoverCardA preview of where a link goes, on hover or focus; nothing essential in itA profile behind an @mention, a repository’s summary
ToastA brief confirmation after an action, which goes away by itself“Saved”, “Archived — Undo”
AlertA message that stays in the page until the situation changes“Your trial ends in 3 days”, a form error summary
import { Button, Toast, useToastManager } from '@arun-dev/ui';
// The Provider and Viewport go near your app's root, once. Here they wrap the example.
export default function ToastBasics() {
return (
<Toast.Provider>
<Buttons />
<Toast.Viewport />
</Toast.Provider>
);
}
function Buttons() {
const toast = useToastManager();
function archive() {
const id = toast.add({
title: 'Conversation archived',
type: 'success',
action: {
label: 'Undo',
onClick: () => {
toast.close(id);
toast.add({ title: 'Restored', timeout: 2000 });
},
},
});
}
function save() {
// One toast, updated when the work finishes.
const id = toast.add({ title: 'Saving…', timeout: 0 });
setTimeout(() => toast.update(id, { title: 'Saved', type: 'success', timeout: 3000 }), 1200);
}
function fail() {
toast.add({
title: 'Connection lost',
description: 'Your changes will sync when you are back online.',
type: 'error',
priority: 'high',
});
}
return (
<div style={{ display: 'flex', gap: 'var(--space-2xs)', flexWrap: 'wrap' }}>
<Button onClick={archive}>Archive</Button>
<Button onClick={save}>Save</Button>
<Button onClick={fail}>Go offline</Button>
</div>
);
}

A toast is a brief message that stacks at the edge of the window, the bottom right by default, and closes itself after a few seconds: “Saved”, “Archived — Undo”.

Put the Provider and one Viewport near your app’s root. Then call useToastManager() anywhere below to show a toast:

import { Toast, useToastManager } from '@arun-dev/ui';
<Toast.Provider>
<App />
<Toast.Viewport />
</Toast.Provider>;
function SaveButton() {
const toast = useToastManager();
return <Button onClick={() => toast.add({ title: 'Saved', type: 'success' })}>Save</Button>;
}
OptionTypeDoes
titleReactNodethe headline
descriptionReactNodethe detail, under it
typestringinfo, success, warning or error colour the edge
timeoutnumbermilliseconds before it closes; 0 keeps it until closed
priority'low' | 'high'high is read out at once (role="alert"); low, when idle
action{ label, onClick }a button on the toast, such as “Undo”. It does not close the toast
onClose() => voidcalled once, however it closed
positionToastPositionwhere this toast shows, instead of the Viewport’s position
idstringreuse one to replace a toast

add returns the id. Pass it to update to change the text when work finishes, or to close.

Toasts show at the bottom right by default. Set position on the Viewport to change it for every toast, and pass position to add to send one toast somewhere else:

<Toast.Viewport position="top-center" />;
toast.add({ title: 'Link copied', position: 'bottom-left' });

The six positions are top-left, top-center, top-right, bottom-left, bottom-center and bottom-right. Toasts at the top slide down as they arrive, and toasts at the bottom slide up. A toast with its own position joins a stack at that position, which is still part of the same toast area, so pausing, Esc and screen-reader announcements work the same.

import { useState } from 'react';
import { Button, Select, Toast, useToastManager, type ToastPosition } from '@arun-dev/ui';
const positions: ToastPosition[] = [
'top-left',
'top-center',
'top-right',
'bottom-left',
'bottom-center',
'bottom-right',
];
// The Viewport's `position` is the default for every toast; one toast can still choose its own.
export default function ToastPositionExample() {
const [position, setPosition] = useState<ToastPosition>('top-center');
return (
<Toast.Provider>
<div style={{ display: 'flex', gap: 'var(--space-2xs)', flexWrap: 'wrap' }}>
<Select
aria-label="Default position"
value={position}
onChange={(event) => setPosition(event.target.value as ToastPosition)}
>
{positions.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</Select>
<Buttons />
</div>
<Toast.Viewport position={position} />
</Toast.Provider>
);
}
function Buttons() {
const toast = useToastManager();
return (
<>
<Button onClick={() => toast.add({ title: 'Saved', type: 'success' })}>
Toast at the default
</Button>
<Button
onClick={() => toast.add({ title: 'Link copied', type: 'info', position: 'bottom-left' })}
>
Toast at bottom-left
</Button>
</>
);
}

Choose a default for all toasts in the menu, then show one at the default and one at bottom-left, which overrides it.

  • Timers pause while the pointer is over the toasts, while focus is in them, and while the page is hidden. They resume with the time that was left.
  • Esc closes the toast that has focus. Focus moves to the toast area, not the page.
  • Screen readers hear new toasts politely, or at once with priority: 'high'.
  • The newest three show by default (limit). The rest wait their turn.
  • The toast area sits in the browser’s top layer, so toasts draw above everything. While a modal Dialog is open they can be read but not clicked. Everything outside a modal dialog is inert. Give toasts that may appear then a timeout.

Pass the Viewport a function of the toasts and lay each out with the parts:

<Toast.Viewport>
{(toasts) =>
toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast}>
<Toast.Title />
<Toast.Description />
<Toast.Close aria-label="Dismiss" />
</Toast.Root>
))
}
</Toast.Viewport>
Toast.Provider props
PropTypeDefaultDescription
limitnumber3How many show at once. The rest wait, their timers stopped, and show as others close.
timeoutnumber5000Milliseconds a toast stays, unless it sets its own. 0 keeps toasts until closed.
Toast.Viewport props
PropTypeDefaultDescription
closeLabelstring'Dismiss'Accessible name of each toast's close button, in the default rendering.
position'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right''bottom-right'Where toasts show, unless a toast sets its own position when added.
children(toasts) => ReactNodeLay the toasts out yourself with Toast.Root and its parts. Omit it for the default rendering.
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM.
  • Keep toasts short, and never the only way to do something. A toast can vanish before anyone reaches its Undo, so offer the same action elsewhere.
  • For a message that stays on the page, use an Alert.
  • Restyle with the --toast-* tokens.