Skip to main content
WCAG Patterns

WCAG 4.1.3 · Level AA · WCAG 2.1

Status Messages

Status messages can be programmatically determined through role or properties so they're announced by assistive tech without receiving focus.

Principle
Robust
Guideline
Compatible
Level
AA
Added in
WCAG 2.1

What it really means

Status messages — "Added to cart", "3 results found", "Saved", "You have unread messages" — must be programmatically determined through role or ARIA properties, so assistive tech announces them without the user having to move focus to find them.

The rule was introduced in WCAG 2.1 to close a long-standing gap: a sighted user sees a toast pop in from the corner, but an AT user typing in a search field hears nothing unless something takes focus — which would disrupt their typing.

Who it helps

  • Screen-reader users, who hear updates without chasing focus.
  • Braille users, whose display reflects the live region.
  • Cognitive-disabled users, who benefit from consistent feedback timing.

How to test

  1. Turn on VoiceOver or NVDA. Trigger the status — add to cart, filter results, save a draft. Verify the screen reader speaks the message without you moving focus.
  2. Confirm the element carries an appropriate role or aria-live value, and was not empty at page load.
  3. Don't role="alert" for normal status — that's assertive and cuts off other announcements. Reserve alert for errors and warnings.

The three primary roles

RoleLive-region levelUse for
statuspoliteGeneral success ("Saved"), search counts, non-critical updates.
logpoliteChat messages, transcripts, append-only feeds.
alertassertiveErrors, session-expiring warnings, destructive-action confirmations.

You can also use generic aria-live="polite" or aria-live="assertive" on a region if the role doesn't fit semantically.

A failing pattern

// Visual-only toast; screen readers hear nothing.
<div className="toast">Added to cart</div>
 
// "3 results" update in a generic div — no live-region contract.
<div className="results-count">{count} results</div>

A passing pattern

// Polite status — announced after current AT speech, no interrupt.
<div role="status" aria-live="polite">
  {statusMessage}
</div>
 
// Search result counts — update the same region on every change.
function ResultsCount({ count }: { count: number }) {
  return (
    <p role="status" aria-live="polite" aria-atomic="true">
      {count === 0
        ? "No results match your filters."
        : `${count} result${count === 1 ? "" : "s"} found.`}
    </p>
  );
}
 
// Assertive — use sparingly for errors.
<div role="alert">Session expired. Please sign in again.</div>

Key rules:

  1. The element with the role must exist in the DOM before it is populated. Mounting an empty <div role="status"> on first render and updating its content later is reliable. Injecting a fresh element with a message already in it is flaky in some screen readers.
  2. aria-atomic="true" replaces the whole region on update — useful for "3 results" counters. Without it, AT may announce just the diff.
  3. Don't overuse live regions. Every polite announcement queues up; three of them at once create a wall of speech.

Notes and edge cases

  • Toasts that disappear quickly must stay long enough for AT to announce them. A 3-second toast often fails. Pair with an error summary for anything the user needs to act on.
  • Form errors sometimes use role="alert" on the error paragraph — that's fine for a single field but noisy for a whole form. Use an error summary instead.
  • Toasts with actions (Undo link) must not auto-dismiss before AT users can reach them. Provide a visible control and don't rely on hover-to-pause.