Toast accessibility
Toast is built on sonner, which ships with WCAG-aware defaults. The MiHCM wrapper preserves them and adds token-based colour contrast.
Live region
- The
<Toaster />mounts a single region (<section role="region" aria-label="Notifications">) so assistive tech announces new toasts without page nav. - Each row uses
aria-live:politefortoast(),toast.success(),toast.warning(),toast.info(),toast.message().assertivefortoast.error()— failures get announced immediately.
toast.loading()andtoast.promise()rows update in place when re-keyed with the sameid, so screen readers don't re-announce while a task progresses.
Keyboard
- The Toaster region is focusable via the
alt+Tshortcut (sonner's default — configurable via thehotkeyprop). - Once focused, Tab cycles into the toast row's action / cancel / close buttons.
- Esc closes the focused row.
- Toasts never steal focus from the user's current task — opening a toast doesn't move focus.
Timing
- Default auto-dismiss is 5 s (configurable). Hovering or focusing a row pauses the timer, so users with slower reading speeds can finish before dismissal — satisfies WCAG 2.2.1 (Timing Adjustable).
- For critical information that requires user action, use an Alert or AlertDialog instead — toasts auto-dismiss by design.
- Pass
duration: Infinityfor a row that only dismisses on user action.
Colour is never the only signal
- Type variants tint the surface (success / error / warning / info) but the title text always carries the primary meaning. Pair with descriptive copy ("Saved", "Network error") rather than a generic "Notification".
- Per-type colours come from MiHCM tokens (
--color-success,--color-destructive,--color-warning,--color-primary) so they meet the design system's contrast budget in both light and dark mode.
Touch + pointer
- Each row supports swipe-to-dismiss (horizontal drag) on touch devices and pointer drag on desktop. The interaction starts after a small threshold, so vertical scrolls and stray touches won't dismiss accidentally.
- The optional close button is sized 24 × 24 — comfortably above the WCAG 44 × 44 hit-target threshold when combined with the row's outer padding.
Motion
- Entry/exit transitions respect
prefers-reduced-motion: reduce(sonner reads the media query and shortens / disables animation). No additional configuration needed.
Screen-reader transcript example
Saving…
Saved Q4 report
A toast.promise() row produces those two consecutive announcements — the first when the loading state mounts, the second when the same id is replaced by the resolved value.
Audit checklist
- One
<Toaster />mounted at the app root, not duplicated per page. -
toast.error()used for failures (so the row is announced assertively). - Title is descriptive without leaning on the icon or colour.
- Action button verbs are explicit ("Undo", "Retry") — not "OK" / "Click here".
- No toast is the only indication that a destructive action succeeded — keep a status row in the UI as well.
- Custom JSX toasts (
toast.custom()) include semantic markup if they replace the entire row chrome.
Related
- sonner accessibility notes — upstream behaviour.
- Alert — persistent inline message (does not auto-dismiss).
- AlertDialog — blocking confirmation when the user must respond.