How does dynamic aria-live content work?

  • Inconsistently: You may experience differences in expected behavior
  • The screenreader expects content within an element with a aria-live="polite" attribute to change
  • By default, only the content that has changed will be read
  • To force the screenreader to read all contents even if it did not change within the element, add aria-atomic="true"
  • Rarely must you use aria-live="assertive" as it (is intended to) override every other message from the screenreader

About alerts

  • By default an element using role="alert" has aria-live="assertive"

Focus management & consistency

For actual single page apps:

  • Focus must be deliberately and consistently placed at the
    • top of new page content or
    • top of the HTML page
  • DO NOT place focus on the first input on page load

Code example

aria-live="polite" announces only newly injected content after other messages

aria-live="polite" aria-atomic="true" announces all injected content after other messages

aria-live="assertive" container announces only newly injected content before other messages

aria-live="assertive" aria-atomic="true" announces all injected content before other messages

role="alert" aria-atomic="true" announces all injected content before other messages (like aria-live="assertive")

role="status" aria-atomic="true" announces all injected content after other messages (like aria-live="polite")

<div class="dynamic-app">
  <p><code>aria-live="polite"</code> announces only newly injected content after other messages</p>
  <ol class="dynamic-content" aria-live="polite" aria-atomic="false">
  </ol>
  <button type="button" onclick="showContent()">Update content</button>
</div>

<div class="dynamic-app">
  <p><code>aria-live="polite" aria-atomic="true"</code> announces all injected content after other messages</p>
  <ol class="dynamic-content" aria-live="polite" aria-atomic="true">
  </ol>
  <button type="button" onclick="showContent()">Update content</button>
</div>

<div class="dynamic-app">
  <p><code>aria-live="assertive"</code> container announces only newly injected content before other messages</p>
  <ol class="dynamic-content" aria-live="assertive" aria-atomic="false">
  </ol>
  <button type="button" onclick="showContent()">Update content</button>
</div>

<div class="dynamic-app">
  <p><code>aria-live="assertive" aria-atomic="true"</code> announces all injected content before other messages</p>
  <ol class="dynamic-content" aria-live="assertive" aria-atomic="true">
  </ol>
  <button type="button" onclick="showContent()">Update content</button>

</div>

<div class="dynamic-app">
  <p><code>role="alert" aria-atomic="true"</code> announces all injected content before other messages (like aria-live="assertive")</p>
  <ol class="dynamic-content" role="alert" aria-atomic="true">
  </ol>
  <button type="button" onclick="showContent()">Update content</button>
</div>

<div class="dynamic-app">
  <p><code>role="status" aria-atomic="true"</code> announces all injected content after other messages (like aria-live="polite")</p>
  <ol class="dynamic-content" role="status" aria-atomic="true">
  </ol>
  <button type="button" onclick="showContent()">Update content</button>
</div>

<template>
  <li>Injected content</li>
</template>

<script>
function showContent() {
  let temp = document.getElementsByTagName("template")[0];
  let clone = temp.content.cloneNode(true);
  event.target.parentNode.querySelector('.dynamic-content').appendChild(clone);
}
</script>