Back to Accessibility
Accessibility
medium
mid

How should screen readers announce new toast notifications?

Toasts appear without user action, so screen readers won't notice them unless you use an ARIA live region. Put a permanent live-region container in the DOM (role='status'/aria-live='polite' for info, role='alert'/aria-live='assertive' for errors) and inject toast text into it. Don't move focus to the toast, and keep it on screen long enough to be read.

6 min read·~10 min to think through

A toast is a notification that appears without the user doing anything — which is exactly why it's an accessibility trap. A sighted user sees it slide in; a screen reader user has no idea it exists unless you explicitly announce it.

The mechanism: ARIA live regions

A live region is an element the screen reader watches. When its content changes, the SR announces the change without moving focus.

html
<!-- Mounted ONCE, lives for the app's lifetime, usually empty -->
<div id="toast-region" aria-live="polite" aria-atomic="true"></div>

When a toast fires, you inject its text into that region:

js
function announce(message) {
  document.getElementById("toast-region").textContent = message;
}

Critical rule: the live-region container must already be in the DOM before the content changes. If you mount the container and its text in the same render, many screen readers miss it.

politeness: polite vs assertive

TypeAttributeWhen
Info / successaria-live="polite" or role="status"Waits for a pause, then announces. "Item added to cart."
Error / urgentaria-live="assertive" or role="alert"Interrupts immediately. "Payment failed."

Default to polite. Reserve assertive for things the user must hear now — overusing it makes the app feel like it's shouting.

role="status" implies aria-live="polite"; role="alert" implies aria-live="assertive". Using the role is often cleaner.

A React pattern

tsx
function ToastRegion({ toasts }: { toasts: Toast[] }) {
  return (
    <>
      {/* Visible toasts */}
      <div className="toast-stack" aria-hidden="true">
        {toasts.map((t) => <ToastCard key={t.id} toast={t} />)}
      </div>
      {/* The announcer — visually hidden, always mounted */}
      <div className="sr-only" role="status" aria-live="polite">
        {toasts.filter((t) => t.type !== "error").map((t) => t.message).join(", ")}
      </div>
      <div className="sr-only" role="alert" aria-live="assertive">
        {toasts.filter((t) => t.type === "error").map((t) => t.message).join(", ")}
      </div>
    </>
  );
}

Note the visible stack is aria-hidden so the toast text isn't announced twice (once visually-mapped, once via the region).

Other rules

  • Do NOT move focus to the toast. It's non-modal; stealing focus interrupts what the user is doing. (A modal alert dialog is different.)
  • Don't auto-dismiss too fast. WCAG 2.2.1: give the user enough time. ~5s minimum for short messages; for important ones, let the user dismiss manually and don't auto-dismiss at all. If a toast has an action ("Undo"), it must be keyboard-reachable, which means it can't disappear on a timer the user can't control.
  • .sr-only must not be display:none — that removes it from the accessibility tree. Use the clip/position technique.
  • aria-atomic="true" makes the SR read the whole region, not just the diff — usually what you want for a single message.

Senior framing

The senior answer nails three things: (1) toasts are silent to SR users by default, so a persistent live region is mandatory; (2) polite vs assertive is a deliberate UX choice, not a default; (3) you don't trap or move focus for a non-modal toast, but you also respect WCAG timing so the message and any action stay reachable. Bonus points for knowing the container must pre-exist in the DOM.

Follow-up questions

  • Why must the live-region container exist in the DOM before the message is added?
  • When is an alert dialog (with focus trap) the right choice instead of a toast?
  • How do you handle a toast with an 'Undo' action accessibly?
  • What does aria-atomic do?

Common mistakes

  • Mounting the toast text and its live-region container in the same render — the change isn't announced.
  • Moving focus to the toast, interrupting the user's task.
  • Using assertive for every toast, making the SR constantly interrupt.
  • Auto-dismissing before a screen reader user can hear it or reach its action button.
  • Using display:none for .sr-only, which hides it from assistive tech entirely.

Edge cases

  • Rapid-fire toasts can overwhelm the announcer — debounce or queue messages.
  • A toast with an action button needs the toast to persist until dismissed.
  • Duplicate announcements if both the visible toast and the live region are exposed to AT.

Real-world examples

  • 'Added to cart' confirmations, 'Message sent', form save indicators, 'Connection lost' banners.

Related questions