4.1.3 Level AA Guideline 4.1 Compatible Added in WCAG 2.1 Partly detectable automatically

Status Messages

Status messages can be programmatically determined through role or properties so they are announced by assistive technology without receiving focus.

Who this affects

Screen reader users, who otherwise never learn that a search returned 12 results, that an item was added to the basket, or that a form saved. The message appears on screen and is silently invisible to them.

The failure and the fix

Each example below is a self-contained page, loaded in a frame so its markup cannot affect this page. Open either one on its own to test it with a keyboard or screen reader.

Fails 4.1.3

Open the failing example in a new tab

fail.html — the problem

<h1>Search the catalogue</h1>

<form id="f">
  <div class="field">
    <label for="q">Search</label>
    <input type="search" id="q" value="tide">
  </div>
  <button type="submit" class="button">Search</button>
</form>

<!-- FAILURE 1: the result count appears on screen and is never announced.
     A screen reader user presses Search and hears silence. They have no idea
     whether the search ran, found nothing, or is still loading. -->
<p id="count"></p>

<!-- FAILURE 2: the same for add-to-basket confirmation. -->
<div class="result">
  <p>Tidal Flats, by A. Marsh</p>
  <button type="button" class="button button--secondary" onclick="add()">Add to basket</button>
</div>
<p id="basket"></p>

<!-- FAILURE 3: a live region created at the same moment the message is put
     into it. This is the subtle one, and it is everywhere: a live region must
     already exist in the DOM, and be being observed, BEFORE the text changes.
     Injecting <p role="status">Saved</p> announces nothing in most screen
     readers. -->
<div id="late"></div>
<p><button type="button" class="button button--secondary" onclick="saveLate()">Save for later</button></p>

Meets 4.1.3

Open the passing example in a new tab

pass.html — the fix

<h1>Search the catalogue</h1>

<form id="f">
  <div class="field">
    <label for="q">Search</label>
    <input type="search" id="q" value="tide">
  </div>
  <button type="submit" class="button">Search</button>
</form>

<!-- FIX 1: the live region is in the DOM from page load, EMPTY. The screen
     reader starts observing it immediately, so when text is written into it
     later the change is announced.

     role="status" implies aria-live="polite" and aria-atomic="true": the
     message waits for a natural pause instead of interrupting. -->
<p id="count" role="status"></p>

<div class="result">
  <p>Tidal Flats, by A. Marsh</p>
  <button type="button" class="button button--secondary" onclick="add()">Add to basket</button>
</div>

<!-- FIX 2: a second, separate live region. Two independent messages need two
     regions, or one overwrites the other before it is read. -->
<p id="basket" role="status"></p>

<!-- FIX 3: the region exists up front here too. -->
<p id="late" role="status"></p>
<p><button type="button" class="button button--secondary" onclick="saveLate()">Save for later</button></p>

<!-- Choosing the right politeness:
       role="status"  / aria-live="polite"    - waits for a pause. Use this for
                                                almost everything: search results,
                                                save confirmations, counts.
       role="alert"   / aria-live="assertive" - interrupts immediately. Reserve
                                                it for errors and things the user
                                                must act on now. Overusing it makes
                                                a page hostile to listen to.

     And the boundary of 4.1.3: it applies to messages that do NOT take focus.
     If you move focus to the message - as an error summary does - then focus
     does the announcing and a live region is unnecessary. -->

How to test it

Trigger each status update — search results, validation summaries, add-to-cart confirmations, loading states, auto-save notices. Confirm each is inside a live region that exists in the DOM before the message arrives, and confirm a screen reader actually announces it.

  • Automated

    Scanners validate live region attributes when present. They cannot detect a status message that has no live region — the actual failure — and cannot verify announcement really happens.

    This demo's failure is not machine-detectable. There is nothing for a rule to fire on. axe validates live-region attributes when they are present; the failure here is that no live region EXISTS, so the scanner sees an ordinary paragraph. A scanner also cannot press the button, so it never observes the update at all. 4.1.3 has to be tested by triggering each status change with a screen reader running.

    Relevant axe rules:

    • aria-valid-attr-value
    • aria-allowed-attr
  • Keyboard

    Unplug the mouse. Move through the page with Tab, Shift+Tab, Enter, Space and the arrow keys. See the keyboard testing script.

  • Screen reader

    Listen to both examples with NVDA, JAWS, VoiceOver or TalkBack and compare what is announced. See the screen reader cheat sheets.

  • Visual

    Zoom to 200% and 400%, narrow the viewport to 320 px, and apply the text-spacing overrides. See the visual testing procedures.

In the specification