Tutorial
Status messages that actually arrive
2 min read
In short
A status message has to be announced without moving focus. That needs a live region already present in the DOM before the message arrives: role="status" (equivalent to aria-live="polite"). If the container is inserted together with its text, many screen readers hear nothing.
At a glance
Create an empty live region
Put one <div role="status"> in the page, empty and visually hidden.
Only set the text
On an event, change the text content of the existing region rather than rebuilding it.
Polite rather than assertive
Confirmations are polite. assertive interrupts what is being read and is for urgent things only.
Test with a screen reader
Trigger the action without looking: is the message announced, and did focus stay put?
Criterion 4.1.3 is the one people notice last, because nothing looks broken: the toast appears, the result count updates, the filter applies. Only nobody who is not looking at that spot learns about it. These are exactly the cases our dynamic-content layer finds, by triggering the interaction and observing what happens semantically afterwards.
The three typical mistakes
<!-- 1. Toast wird samt Text neu ins DOM gehaengt:
oft kommt gar keine Ansage an, weil die Live-Region
im selben Moment erst entsteht -->
<script>
const toast = document.createElement("div");
toast.className = "toast";
toast.textContent = "In den Warenkorb gelegt";
document.body.appendChild(toast);
</script>
<!-- 2. Ergebniszahl aendert sich stumm -->
<p class="results">42 Treffer</p>
<!-- 3. Alles laut: assertive fuer eine harmlose Bestaetigung
unterbricht die laufende Ausgabe -->
<div aria-live="assertive">Filter angewendet</div>The first case is the most common and the most treacherous: the live region comes into existence at the same moment as its content. Many screen readers only watch regions that already existed, so nothing is reported. An audit report will call this „live region inserted with content“, and this is what it means.
The reliable implementation
<!-- Die Live-Region steht von Anfang an im DOM und ist leer.
Spaeter wird nur ihr Text gesetzt: das loest die Ansage aus. -->
<div id="status" role="status" aria-live="polite" class="sr-only"></div>
<p class="results">
<span id="result-count" role="status">42 Treffer</span>
</p>
<script>
function announce(message) {
const region = document.getElementById("status");
// Leeren und im naechsten Frame setzen: identische Texte
// hintereinander werden sonst nicht erneut angesagt.
region.textContent = "";
requestAnimationFrame(() => {
region.textContent = message;
});
}
announce("In den Warenkorb gelegt");
</script>/* Visuell versteckt, aber im Accessibility-Tree vorhanden.
Nicht display:none verwenden: das entfernt die Region komplett. */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}polite, assertive or focus
- role="status" (polite): confirmations, result counts, loading states. Waits until the current output has finished.
- role="alert" (assertive): errors and warnings that must arrive immediately. It interrupts, so use it sparingly.
- A focus move: for events where the operating position should change too, such as an error summary after submission.
Details that make the difference
- The same text twice in a row is often not re-announced: clear it first, then set it in the next frame.
- Keep the message short and complete: „Added to basket: running shoe 4711“ beats „Success“.
- If a toast disappears after three seconds, the information is gone for slower output. Keep important messages, or repeat them somewhere permanent.
- One live region per purpose is enough. Several at once overlap.
Testing
- Start a screen reader, trigger the action from the keyboard, look away: does an announcement come?
- Check that focus stayed where it was (status messages must not steal it).
- Trigger the same action twice: is it announced the second time too?
Related WCAG criteria
Frequently asked questions
- Why is nothing heard even though aria-live is set?
- Usually because the region entered the DOM together with its text, or because it is hidden with display:none. The region has to exist beforehand, visible or sr-only hidden, and only its content may change.
- What is the difference between role="status" and aria-live="polite"?
- Practically none: role="status" brings aria-live="polite" and aria-atomic="true" with it. The role is the shorter and more robust spelling.
- Does a progress bar count as a status message?
- The change from „loading“ to „done“ is a status message and should be announced. For the progress itself use role="progressbar" with an updated aria-valuenow, otherwise every percent is spoken.
Request a free scan
Tell us your domain - we run a free initial scan and show you the most important barriers.