Accessibility pattern · Feedback and status

Alert banner

Alerts already on the page are plain content: a tone word, an icon and a heading, read in order like the rest of the page. Only an error that happens later, like a failed payment, goes into a role=alert container that was in the page from the start, so it is announced at once and focus stays put.

WCAG criteria
6
Keyboard rules
3
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Billing

Studio plan, paid yearly

Info: Scheduled maintenance

Card payments pause on Sunday 12 October, from 2 to 4 a.m. IST. UPI keeps working.

See service status

Success: Card added

Your HDFC Bank card ending 4418 now pays for this plan.

Warning: Your plan renews in 3 days

₹4,999 will be charged on 8 October. Change or cancel the plan before then if you need to.

Review your plan

Error: GST number not verified

The GSTIN on file does not match your business name, so it cannot appear on your invoices.

Fix GST details

Amount due

₹4,999

In this demo the payment always fails, so you can see the alert.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Tab or ShiftTabMoves through the links and buttons inside the alerts; the alerts themselves are not tab stops.
Enter or SpaceOn Pay ₹4,999, tries the payment. The error appears below and is announced at once, while focus stays on the button.
Enter or SpaceOn the dismiss button, removes the maintenance notice and moves focus to the alert that takes its place.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
The page loadsNothing: alerts already on the page are read in order, never announced
Reading reaches the warningWarning: Your plan renews in 3 days, heading level 4
Pay ₹4,999 failsError: Payment failed. Your bank declined the card ending 4418… (at once, interrupting)
Focus reaches the dismiss buttonDismiss Scheduled maintenance, button
The notice is dismissedSuccess: Card added, group (focus is on the next alert)

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-alert" data-ap-alert>
  <div class="ap-alert__top">
    <h3 class="ap-alert__intro" id="alert-billing" tabindex="-1">Billing</h3>
    <p class="ap-alert__lead">Studio plan, paid yearly</p>
  </div>

  <div class="ap-alert__stack">
    <div class="ap-alert__item ap-alert__item--info" role="group" aria-labelledby="alert-info-h">
      <span class="ap-alert__mark" aria-hidden="true"><svg class="ap-alert__icon" viewBox="0 0 24 24" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 7.5h.01"/></svg></span>
      <div class="ap-alert__body">
        <h4 class="ap-alert__name" id="alert-info-h"><span class="ap-alert__tone">Info:</span> <span id="alert-info-name">Scheduled maintenance</span></h4>
        <p class="ap-alert__text">Card payments pause on Sunday 12 October, from 2 to 4 a.m. IST. UPI keeps working.</p>
        <a class="ap-alert__link" href="#alert-status">See service status</a>
      </div>
      <button type="button" class="ap-alert__dismiss" id="alert-info-x" aria-labelledby="alert-info-x alert-info-name" data-ap-dismiss>
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
        <span class="ap-alert__sr">Dismiss</span>
      </button>
    </div>

    <div class="ap-alert__item ap-alert__item--success" role="group" aria-labelledby="alert-success-h">
      <span class="ap-alert__mark" aria-hidden="true"><svg class="ap-alert__icon" viewBox="0 0 24 24" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.7 2.7L16.2 9.6"/></svg></span>
      <div class="ap-alert__body">
        <h4 class="ap-alert__name" id="alert-success-h"><span class="ap-alert__tone">Success:</span> <span>Card added</span></h4>
        <p class="ap-alert__text">Your HDFC Bank card ending 4418 now pays for this plan.</p>
      </div>
    </div>

    <div class="ap-alert__item ap-alert__item--warning" role="group" aria-labelledby="alert-warning-h">
      <span class="ap-alert__mark" aria-hidden="true"><svg class="ap-alert__icon" viewBox="0 0 24 24" focusable="false"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg></span>
      <div class="ap-alert__body">
        <h4 class="ap-alert__name" id="alert-warning-h"><span class="ap-alert__tone">Warning:</span> <span>Your plan renews in 3 days</span></h4>
        <p class="ap-alert__text">₹4,999 will be charged on 8 October. Change or cancel the plan before then if you need to.</p>
        <a class="ap-alert__link" href="#alert-plan">Review your plan</a>
      </div>
    </div>

    <div class="ap-alert__item ap-alert__item--error" role="group" aria-labelledby="alert-error-h">
      <span class="ap-alert__mark" aria-hidden="true"><svg class="ap-alert__icon" viewBox="0 0 24 24" focusable="false"><path d="M7.9 2.5h8.2l5.4 5.4v8.2l-5.4 5.4H7.9l-5.4-5.4V7.9Z"/><path d="m15 9-6 6M9 9l6 6"/></svg></span>
      <div class="ap-alert__body">
        <h4 class="ap-alert__name" id="alert-error-h"><span class="ap-alert__tone">Error:</span> <span>GST number not verified</span></h4>
        <p class="ap-alert__text">The GSTIN on file does not match your business name, so it cannot appear on your invoices.</p>
        <a class="ap-alert__link" href="#alert-gst">Fix GST details</a>
      </div>
    </div>
  </div>

  <div class="ap-alert__pay">
    <div class="ap-alert__due">
      <p class="ap-alert__k">Amount due</p>
      <p class="ap-alert__amount">₹4,999</p>
      <p class="ap-alert__hint">In this demo the payment always fails, so you can see the alert.</p>
    </div>
    <button type="button" class="ap-btn ap-btn--primary" data-ap-pay>Pay ₹4,999</button>
  </div>
  <div class="ap-alert__live" role="alert" data-ap-live></div>

  <template data-ap-failed>
    <div class="ap-alert__item ap-alert__item--error">
      <span class="ap-alert__mark" aria-hidden="true"><svg class="ap-alert__icon" viewBox="0 0 24 24" focusable="false"><path d="M7.9 2.5h8.2l5.4 5.4v8.2l-5.4 5.4H7.9l-5.4-5.4V7.9Z"/><path d="m15 9-6 6M9 9l6 6"/></svg></span>
      <div class="ap-alert__body">
        <h4 class="ap-alert__name"><span class="ap-alert__tone">Error:</span> <span>Payment failed</span></h4>
        <p class="ap-alert__text">Your bank declined the card ending 4418, and you have not been charged. Try another card, or pay by UPI.</p>
      </div>
    </div>
  </template>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Each alert has a real heading that starts with its tone, so the alerts show up in a screen reader's list of headings with their meaning.

  • 1.4.1 Use of Color Level A

    Info, Success, Warning and Error are written out and each has its own icon shape; the tint only repeats what the word says.

  • 2.4.3 Focus Order Level A

    When the notice is dismissed, focus moves to the alert that takes its place instead of falling back to the top of the page.

  • 3.3.1 Error Identification Level A

    The failed payment is described in words: what went wrong, that no money was taken, and what to try next.

  • 4.1.2 Name, Role, Value Level A

    The dismiss button's name is Dismiss plus the alert's heading, so it says which alert it closes.

  • 4.1.3 Status Messages Level AA

    The payment error arrives in a role=alert container and is announced without focus moving to it.

Usage

When to use it

Use it

  • Something on the page needs attention: a problem, a risk, a change in service, or a sign that a step worked.
  • An error caused by something the person just did, like a failed payment, that they must hear about now.

Use something else

  • Routine confirmations such as Saved: use a polite status message or a toast, which do not interrupt.
  • Errors in form fields: show them next to the field and in an error summary, as in the form errors pattern.
  • Anything that needs an answer before going on: use an alert dialog.

Common failures

How it usually goes wrong

  • role=alert on alerts already there

    An alert that is in the page when it loads is either read over everything else or not at all, depending on the browser. Static alerts are plain content here; only the payment error, which happens later, is live.

  • Adding the live region with its message

    A role=alert element inserted with its text already inside is missed by some screen reader and browser pairs. The empty container is in the HTML from the start, and only its content changes.

  • Moving focus to the message

    Focus that jumps to an error pulls people away from what they were doing and loses their place. role=alert is heard wherever focus is.

  • Tone shown by color alone

    A red box and a green box look alike to many people with color vision deficiencies, and to every screen reader. Each alert starts with its tone in words.

  • A row of buttons called Close

    With several alerts, Close, Close, Close says nothing about what goes. Each dismiss button is named Dismiss plus its alert's heading.

  • Assertive for everything

    Every assertive message cuts off what a screen reader was saying. Keep role=alert for errors and risks; confirmations go in a polite status.

Notes

Building it

  • role=alert means aria-live="assertive" plus aria-atomic="true": it interrupts and reads the whole message. role=status is the polite version, read at the next pause. aria-live on its own sets the politeness without a role, for an element that already has one, like a list.
  • A live region only announces changes made after the browser knows it is there. Put the empty container in the HTML, then add the message; a region added together with its text, or one shown from display: none at the same moment, is often missed. That is why the container here is never hidden, only empty.
  • To announce the same error twice, empty the container and add the message a moment later: replacing text with the same text may not count as a change.
  • Remember a dismissed notice, in storage or the account, so it stays gone. Never let people dismiss an error while the problem is still there.
  • The links in the alerts use # addresses in the demo, where they stand for other pages.

Sources: WAI-ARIA Authoring Practices: Alert · WAI-ARIA 1.2: the alert role · Understanding SC 4.1.3: Status Messages

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction