Toast
Transient floating notifications built on the Base UI Toast primitive — variants, stacking, promise flows, timer pause, swipe and full keyboard access.
Examples
Default
A basic toast with a title, description, and dismiss button. Timers pause while you hover or focus the stack, and resume when you leave.
Intents
Semantic colors and icons for success, error, warning, and info.
Error toasts announce assertively (a visually-hidden role="alert" clone) and
persist until dismissed; everything else announces politely.
Variants
Four materials: card (default floating panel), tinted (the intent paints
the plate), inverse (the always-dark overlay plate — the snackbar), and
pill (a single-line capsule that sizes to its content, ignores rounded,
and drops description).
Sizes
Compact sm and standard md, plus the independent width cap.
With action
Pair a toast with an inline CTA. Action-bearing toasts default to a 10s duration (instead of 5s) so the action stays reachable — and the same operation must also exist somewhere permanent in your UI, because a toast is ephemeral by design.
Undo
The classic destructive-action follow-up: an inverse snackbar whose Undo is
a regular action button (the on-color chip on the dark plate).
Promise
promise() binds a toast to an async lifecycle: a persistent loading toast
with a spinner, swapped in place for the success or error content when the
promise settles. The error phase persists until dismissed.
Stacking
Toasts collapse into a peeking stack; hover, focus, or F6 expands
it. limit on the provider caps how many are on screen — the overflow stays
mounted (data-limited, inert) and resurfaces as newer toasts leave.
Positions
Six anchors. Below the sm breakpoint every anchor renders as a full-width
strip on its edge — the dominant phone presentation. Swipe-to-dismiss is
derived per position: away from the anchor edge, plus toward the nearer side.
Persistence & dismissal
Errors persist by default; duration: 0 makes anything persistent;
dismissible={false} removes the close button; closeLabel localizes it.
End content
endContent is a free-form trailing slot rendered beside action. When a
close button is also present, the trailing row moves below the text and wraps
rather than overflowing. Match the toast's rounded and button size on your
own controls — the toast renders its action at xs (size="sm") or sm
(size="md").
Deduplication & update
Pass a stable id to keep a repeated action from stacking duplicates
(collisionBehavior: "replace" | "ignore"), or change an on-screen toast with
update() — content, intent, and duration swap in place without replaying the
enter animation.
Title only
A minimal toast that omits the description for short, single-line feedback —
or reach for variant="pill", which is built for exactly this.
RTL
Per-toast dir (or defaultDir on the Toaster) mirrors the layout — icon,
text and controls flip; closeLabel localizes the dismiss button.
Loading
Toast-shaped skeleton placeholders for queued or loading notifications. The control bars derive from the real button variants, so sizes, radii and the mobile touch floors always match.
Usage
import { Toast, ToastProvider, Toaster, useToast } from "@tessinaui/ui/toast";Setup
Wrap your app (or layout) with ToastProvider and place Toaster where you
want notifications to appear. Mount them once, at the root — the viewport is
the live region, and it must exist before toasts are fired.
export default function RootLayout({ children }) {
return (
<ToastProvider limit={3}>
{children}
<Toaster position="bottom-right" />
</ToastProvider>
);
}Firing a toast
function MyComponent() {
const { toast } = useToast();
return (
<button onClick={() =>
toast({
title: "Changes saved",
description: "Your profile has been updated successfully.",
intent: "success",
action: { label: "Undo", onClick: handleUndo },
})
}>
Save
</button>
);
}Promise lifecycles
const { promise } = useToast();
promise(saveDraft(), {
loading: "Saving…",
success: (draft) => ({ title: "Saved", description: draft.name }),
error: (e) => ({ title: "Save failed", description: String(e) }),
});Outside React
createToastManager() gives you the same API in plain modules — wire its
.manager into the provider:
// toasts.ts
import { createToastManager } from "@tessinaui/ui/toast";
export const appToasts = createToastManager();
// layout.tsx
<ToastProvider toastManager={appToasts.manager}>…</ToastProvider>
// anywhere, no hook needed
appToasts.toast({ title: "Session expired", intent: "warning" });Duration rules
| Case | Default |
|---|---|
| Plain toast | 5000 ms (provider timeout) |
intent="error" | 0 — persists until dismissed |
Has action | 10000 ms — the action stays reachable |
Explicit duration | always wins (0 = persistent) |
Toasts are ephemeral. Never make one the only path to information or an action — the same content must be reachable somewhere permanent.
API
Toast
The static visual card — for rendering toast anatomy inline (docs, previews, custom viewports). Fired toasts render the same anatomy automatically.
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Heading line |
description | ReactNode | — | Supporting body text (ignored by variant="pill") |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Semantic color and default icon |
variant | "card" | "pill" | "tinted" | "inverse" | "card" | Visual material |
size | "sm" | "md" | "md" | Compact or standard density |
width | "narrow" | "default" | "wide" | "default" | Max width of the card |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Corner radius (nested buttons follow it; full is the panel cap, the pill variant is the capsule) |
icon | ReactNode | — | Overrides the default intent icon |
showIcon | boolean | true | Show/hide the leading icon |
action | ToastAction | — | Inline CTA button (one action max) |
endContent | ReactNode | — | Trailing custom content, rendered after action |
dismissible | boolean | true | Render the dismiss (×) button |
closeLabel | string | "Dismiss notification" | Accessible name of the dismiss button |
onClose | () => void | — | Renders the dismiss button and receives its click |
dir | "ltr" | "rtl" | — | Text direction |
className | string | — | Extra class names |
Plus every standard div attribute — style, data-* and aria-* are
forwarded to the root element.
ToastAction
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Button label |
onClick | () => void | — | Click handler (does not auto-dismiss) |
variant | ButtonVariant | "primary" | Button style — "ghost" reads as an inline text action |
ToastProvider
| Prop | Type | Default | Description |
|---|---|---|---|
limit | number | 3 | Max toasts on screen; overflow collapses (data-limited, inert) and resurfaces |
timeout | number | 5000 | Default auto-dismiss ms (0 = persistent) |
toastManager | ToastManager | — | Manager from createToastManager() for imperative use |
Toaster
| Prop | Type | Default | Description |
|---|---|---|---|
position | ToastPosition | "bottom-right" | Screen anchor for the stack |
expand | boolean | false | Keep the stack expanded instead of the collapsed peek |
label | string | "Notifications (F6)" | Accessible name of the notifications region |
defaultVariant | ToastVariant | "card" | Fallback variant |
defaultSize | "sm" | "md" | "md" | Fallback size |
defaultWidth | ToastWidth | "default" | Fallback width (also sizes the stack column) |
defaultRounded | ToastRounded | "md" | Fallback corner radius |
defaultDir | "ltr" | "rtl" | "ltr" | Fallback direction |
maxVisible | number | — | Deprecated — use limit on ToastProvider; this prop is inert |
className | string | — | Extra class names on the viewport |
useToast
const { toast, update, promise, dismiss, dismissAll, toasts } = useToast();| Return | Type | Description |
|---|---|---|
toast | (options: ToastOptions) => string | Shows a toast; returns its id |
update | (id, options: Partial<ToastOptions>) => void | Changes an on-screen toast in place |
promise | (p, { loading, success, error }) => Promise | Loading → success/error toast bound to a promise |
dismiss | (id: string) => void | Closes a toast by id |
dismissAll | () => void | Clears all toasts |
toasts | ToastObject[] | Current toast stack (Base UI shape) |
ToastOptions
Everything Toast accepts visually, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | auto | Stable id for dedupe; also what dismiss()/update() take |
collisionBehavior | "replace" | "ignore" | "replace" | What to do when this id is already on screen |
duration | number | see Duration rules | ms before auto-dismiss; 0 = persistent |
onDismiss | () => void | — | Fired once when the toast closes, by any path |
onRemove | () => void | — | Fired after the exit transition, on unmount |
ToastSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
variant | ToastVariant | "card" | Matches the toast it stands in for |
size | "sm" | "md" | "md" | Density |
width | ToastWidth | "default" | Card width |
rounded | ToastRounded | "md" | Corner radius |
showIcon | boolean | true | Leading icon placeholder |
showTitle | boolean | true | Title bar placeholder |
lines | number | 1 | Description line placeholders |
showAction | boolean | false | Action-button placeholder |
showEndContent | boolean | false | Trailing end-content placeholder |
showDismiss | boolean | true | Close-button placeholder |
Position values
| Value | Description |
|---|---|
"top-left" / "top-center" / "top-right" | Top anchors — toasts enter/exit upward, swipe up |
"bottom-left" / "bottom-center" / "bottom-right" | Bottom anchors (default bottom-right) — enter/exit downward, swipe down |
Accessibility
The behavior layer is the Base UI Toast primitive; the contract below is pinned by the component's test suite.
Pattern. The viewport is a named role="region" live area
(aria-label="Notifications (F6)", polite). Each toast renders as a non-modal
dialog labelled by its title and described by its description;
intent="error" toasts are priority: high — announced assertively through a
visually-hidden role="alert" clone, without stealing focus. No toast ever
takes focus when it appears.
Keyboard.
| Key | Action |
|---|---|
| F6 | Move focus into the notifications viewport (expands the stack); leaving restores prior focus |
| Tab / Shift+Tab | Cycle through toasts and their controls; exiting the viewport returns focus to where you were |
| Esc | Close the focused toast |
Timing (WCAG 2.2.1). Auto-dismiss timers pause on hover, on focus, on window blur, and while touching a toast — and resume on leave. Errors persist until dismissed. Action-bearing toasts hold for 10s.
Swipe is a pointer convenience, never the only path — the close button, Esc and the timeout remain.
Reduced motion. Enter/exit transitions and stack movement are disabled
under prefers-reduced-motion: reduce.
Your part. A toast is ephemeral: any action inside one must also exist
somewhere permanent, and critical errors that demand a decision belong in a
dialog, not a toast. Pass closeLabel and label when localizing.
AT status: VoiceOver smoke pending for this release. NVDA and JAWS:
UNTESTED (no Windows host) — the component stays beta until passes are
recorded.