Skip to content
Navigation

Toast

Transient, non-blocking feedback about something that already happened ("Saved", "Upload failed"). If the user MUST see or act on it, it is not a toast — use Notice (inline) or Dialog (blocking).

Added 14 October 2026

Toast, Notice, or Dialog?

Decide by how much the user must not miss it:

  • Confirmation or status of a completed action, safe to miss, auto-dismisses → Toast. It is announced once and disappears; treat it as a courtesy, never as the only record of an outcome. [a11y]
  • Information that must persist on the surface until resolved (form-level error, a standing warning about the current state) → Notice — it stays in the layout.
  • Something that must block the flow until acknowledged or decided (destructive confirm, required input) → Dialog. Never a toast — it can vanish before it is read.

Rule of thumb: if losing the message would cause a problem, it does not belong in a toast.

Setup — mount once, call from anywhere

Toast is imperative, not a component you place where it should appear. Mount a single toaster at the app root, then trigger toasts from anywhere by calling toast().

React:

import { Toaster, toast } from '@stroeer-labs/design-library/react'

// once, at the app root:
<Toaster />

// anywhere:
toast.success('Campaign saved')

Angular:

import { HlmToasterImports, toast } from '@stroeer-labs/design-library/angular'

// once, at the app root component:
//   imports: [HlmToasterImports]
//   template: `<hlm-toaster />`

toast.success('Campaign saved')

One toaster per app. Mounting it inside a component that re-renders/repeats, or mounting several, gives duplicate or lost toasts. (The showcase mounts one per demo card only to isolate the two framework showcases — not a pattern to copy.)

Types — which one and when

Reach for the plain toast() by default; use a typed variant only when the type adds real meaning (it also sets the icon). Type is carried by an icon and wording, never by color alone. [a11y]

  • toast(msg) — neutral confirmation of a completed action (“Draft saved”).
  • toast.success(msg) — an action succeeded and that success is worth calling out.
  • toast.error(msg) — an action failed. Not for errors the user must fix before continuing — that is an inline Notice on the field/form. An error toast that carries a retry/action should be dismissable and not auto-dismiss, so it can actually be acted on.
  • toast.warning(msg) — a completed action with a caveat (“Saved, but 2 rows skipped”).
  • toast.info(msg) — incidental context; prefer silence if it adds nothing.
  • toast.loading(msg) / toast.promise(p, {...}) — pending async work. Prefer toast.promise so the same toast transitions loading → success/error in place.

Content

  • One line. A toast is a headline, not a paragraph. Put the essential outcome in the message.
  • description (optional) — a single supporting detail (timestamp, count). It renders muted below the message; do not hide anything load-bearing there. See docs/patterns/copy.md.
  • action (optional) — a single reversal/next step, most often Undo. Keep it to one action; a toast is not a menu. The real, non-toast affordance must still exist elsewhere — the action can disappear with the toast. [a11y]
toast('Message sent', {
  description: 'To 1,240 contacts',
  action: { label: 'Undo', onClick: () => unsend() },
})

Config — sensible defaults, tune on the toaster

Set these once on <Toaster /> / <hlm-toaster />; defaults match across both frameworks (position="bottom-right", duration=4000, visibleToasts=3, closeButton=false, richColors=false).

  • position — where toasts stack (top-right, bottom-center, …). One position per app; do not scatter.
  • duration — auto-dismiss ms. Auto-dismiss is in tension with WCAG 2.2.1 Timing Adjustable (Level A): a timed message the user needs must be pausable/extendable. So (a) toasts pause on hover/focus — sonner does this by default, keep it; (b) always leave a manual dismiss (closeButton); (c) a toast carrying an action (Undo) must NOT rely on the ~4s default — give it a long duration or Infinity plus a close button, or move it to a Notice. Rough scale: ~4s plain, up to ~10s with an action. [a11y]
  • closeButton — a manual dismiss. Enable it whenever a toast is long-lived, carries an action, or is an error, so users who can’t act within the auto-dismiss window still can (WCAG 2.2.1). [a11y]
  • theme — follows the app’s .dark class automatically (React: default; Angular: theme="auto"). theme="system" follows the OS media query, NOT the app’s dark mode — do not use it to track the app theme.
  • hotkey — focus-the-toast shortcut, same prop name on both sides (default ⌥/alt+T).

Cross-framework parity notes

  • Same imperative API on both: toast, toast.success/info/warning/error/loading/ promise/dismiss, same { description, action } shape, same defaults.
  • Angular exposes a curated set of toaster inputs; React forwards the full sonner ToasterProps. For per-toast tweaks both take a toastOptions object.
  • Icon set is fixed (lucide, one per type) and identical across frameworks; React lets you override icons, Angular does not.
  • Screen-reader announcement is engine-handled: non-urgent toasts announce politely (role="status"), errors assertively (role="alert"). You don’t wire this. [a11y]

Never

  • Put information the user must read or act on in a toast — it auto-dismisses and may be missed. Use Notice or Dialog. [a11y]
  • Make a toast the only record of an outcome (e.g. the sole error explanation for a failed save).
  • Mount more than one toaster, or mount it inside a repeating/re-rendering component.
  • Stack multiple actions in one toast, or rely on the action as the only way to perform it.
  • Convey the toast’s meaning by color alone — the icon and wording must carry it. [a11y]
  • Use theme="system" expecting it to follow the app’s dark mode.
  • Notice component — persistent, inline messaging that stays on the surface.
  • docs/patterns/copy.md — what the message says: short, outcome-first.
  • docs/patterns/action-hierarchy.md — when an outcome deserves a toast vs a heavier surface.