Toast

A brief, non-blocking notification that confirms something happened — optionally with one action such as Undo — announced to assistive technology and gone on its own.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Toast } from '@design-schema/react';

Examples

Message sent
import { Toast } from '@design-schema/react';

<Toast message="Message sent" />

Props

6 rows
PropTypeDefaultDescriptionPlatforms
message requiredstring—

One sentence saying what happened ("Message sent", "3 files deleted").

web, lit, rn, swiftui
toneneutral | success | warning | dangerneutral

Sets the leading icon; `neutral` has none. Toasts do not use tinted backgrounds — the icon and message carry the tone.

web, lit, rn, swiftui
actionLabelstring—

Label for a single action button ("Undo", "View"). When present the toast stays longer and pauses on hover and focus.

web, lit, rn, swiftui
durationshort | long | persistentshort

`short` ≈ 5s, `long` ≈ 10s (both computed from motion.duration.loop × 6 / × 12 so themes without motion still get sensible times), `persistent` until dismissed. When `actionLabel` is set or `tone` is danger the toast is persistent regardless of this prop (a dev warning notes the override only when `duration` was passed explicitly as `short` or `long` — on Lit, when the property or attribute was assigned; the default never warns. It fires each time a change to `duration`, `actionLabel` or `tone` enters that case, including when the case already held and `duration` changes between `short` and `long`; examples and stories with an action or danger tone pass `persistent` or no `duration`, and a story's shared `meta` args never carry `duration` at all, since copying it onto every instance would mark them all as explicitly assigned). If motion.duration.loop resolves to 0 or cannot be resolved (no theme CSS, jsdom), both durations are treated as persistent. The two durations are computed at region mount from the resolved motion.duration.loop (getComputedStyle on the region on web/Lit), never hardcoded; a toast rendered outside a region, and every toast on native, computes them when it mounts — on native from the theme's token value, which is always a number, so only a resolved 0 reaches the persistent fallback.

web, lit, rn, swiftui
dismissiblebooleantrue

Shows a dismiss button. Persistent toasts are always dismissible.

web, lit, rn, swiftui
toastIdstring—

Stable identity; showing a toast with the same toastId replaces the previous one instead of stacking (named toastId so it does not collide with the DOM `id` on Lit). It never becomes the DOM `id`; on a directly rendered Toast outside the region it is accepted and has no effect.

web, lit, rn, swiftui

Events

2 rows
EventTypeDescriptionPlatforms
onActiononAction / action

The action button was activated. The toast dismisses.

web, lit, rn, swiftui
onDismissonDismiss / dismiss

The toast left the screen: reason `timeout`, `dismiss-button`, `escape`, `action`, `replaced` (a replaced or evicted toast leaves immediately, without its exit transition), or `programmatic` (which plays the exit transition like every reason but `replaced`). It fires when the toast begins to leave — synchronously, at the state change that starts the exit transition, while the toast is still connected so on Lit it still bubbles to the region — and the element is removed once the transition ends, the same split Popover uses. A caller that reacts to the event therefore never waits for `exit`, and the reason is known before the animation rather than after it.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.

Implicit ARIA role: status.

Anatomy

regiontoasticonmessageactionButtondismissButton

What generation guarantees

Live region — Changes are announced through a live region, without moving focus.

Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.

Escape dismisses — Escape closes it from anywhere inside.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.

Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).

No hover-only content — Nothing is reachable by hover alone; keyboard focus reveals the same content.

Toast keyboard support

3 rows
KeyAction
F6Moves focus into the toast region (the first toast's action or dismiss button) from anywhere; F6 again returns to where focus was. (a toast is visible)
EscapeDismisses the focused toast and returns focus. (focus inside a toast)
TabMoves between the action and dismiss buttons, then out of the region. (focus inside a toast)

Contrast pairs

4 rows
ForegroundBackgroundLevel
color.inverse.foregroundcolor.inverse.surfaceAA
color.inverse.linkcolor.inverse.surfaceAA
color.inverse.focuscolor.inverse.surfaceAA (non-text)
color.inverse.status.{tone}color.inverse.surfaceAA (non-text)

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.