| name | aria-live-regions |
| description | Load this skill whenever the project contains dynamic content updates, status messages, alerts, notifications, loading indicators, or any use of aria-live, role="status", role="alert", or role="log". Under no circumstances add or modify live-region markup without applying these rules. Prioritize correct politeness levels and avoid redundant announcements.
|
ARIA Live Regions Accessibility Skill
Canonical source: examples/ARIA_LIVE_REGIONS_BEST_PRACTICES.md in mgifford/ACCESSIBILITY.md
This skill is derived from that file. When in doubt, the example is authoritative.
Apply these rules when implementing any dynamic content updates announced to
assistive technologies.
Only load this skill if the project contains dynamic content updates.
Core Mandate
ARIA live regions announce dynamic content changes to screen reader users who
would otherwise miss updates happening outside their current focus point.
They are powerful and frequently misused. The most common errors are:
- Using
assertive when polite is correct (interrupts the user)
- Injecting the live region dynamically (AT misses the initial announcement)
- Announcing too much or too little
- Using live regions as a substitute for proper focus management
Default to polite. Reach for assertive only when a delay would cause the
user to take a wrong action.
Severity Scale (this skill)
| Level | Meaning |
|---|
| Critical | Dynamic content change conveys no information to AT; user cannot complete task |
| Serious | assertive used for non-urgent updates, interrupting screen reader mid-sentence; live region injected dynamically and missed by AT |
| Moderate | Status updates absent; success/failure not announced; announcements too verbose |
| Minor | Redundant announcements; live region content duplicates visible text unnecessarily |
Assistive Technology Context
Live region support varies significantly. Test with:
| AT | Browser | Behaviour |
|---|
| NVDA | Chrome | Reads polite at the end of the current utterance; assertive interrupts immediately |
| JAWS | Chrome | May buffer multiple rapid polite updates; reads them in sequence |
| VoiceOver | Safari (macOS) | polite queued after current speech; assertive interrupts |
| VoiceOver | Safari (iOS) | Similar to macOS; assertive may not interrupt reliably in all versions |
| TalkBack | Chrome (Android) | polite announced; assertive interrupts; test carefully |
| Voice Control | Any | Live regions not directly relevant to voice control navigation |
| Screen magnification | Any | Live regions may update off-screen; consider whether the user can see the update |
Critical AT notes:
- NVDA+Firefox historically had different live region behaviour than NVDA+Chrome — test both if your audience uses Firefox
- Live regions injected after page load but before content is added are sometimes missed — see the injection timing rule below
- Some AT reads
aria-live content even when the region is visually hidden — this can cause duplicate announcements if used alongside visible text
Critical: The Injection Timing Rule
The live region element must be present in the DOM before content is inserted
into it. This is the most common live region mistake.
<script>
const region = document.createElement('div');
region.setAttribute('aria-live', 'polite');
region.textContent = 'Form submitted successfully.';
document.body.appendChild(region);
</script>
<div aria-live="polite" aria-atomic="true" class="visually-hidden" id="status">
</div>
function announce(message) {
const region = document.getElementById('status');
region.textContent = '';
setTimeout(() => { region.textContent = message; }, 50);
}
The 50ms timeout is a practical workaround for AT that only fires on content
change — if the message is the same as the previous one, clearing first and
re-inserting forces the change event.
Critical: polite vs assertive
aria-live="polite" → waits for the user to finish their current action
aria-live="assertive" → interrupts immediately, even mid-sentence
Use polite for:
- Form submission success or failure
- Search results count updates ("12 results found")
- Cart update ("Item added to cart")
- Character count remaining ("140 characters left")
- Loading complete ("Results loaded")
- Filter/sort updates
Use assertive only for:
- Blocking errors that prevent task completion right now
- Timeout warnings where immediate action is required
- Security or session expiry alerts
assertive used for non-urgent updates is Serious — it interrupts screen
reader users in the middle of reading other content, and can make a page
unusable for heavy AT users.
Serious: role="status" and role="alert" — The Shorthand
Two ARIA roles provide live region behaviour without explicit aria-live:
| Role | Implicit aria-live | Implicit aria-atomic | Use for |
|---|
role="status" | polite | true | Success messages, status updates |
role="alert" | assertive | true | Urgent errors, blocking messages |
<div role="status" class="visually-hidden" id="cart-status"></div>
<div role="alert" class="visually-hidden" id="session-warning"></div>
role="alert" is equivalent to aria-live="assertive" aria-atomic="true".
Use it only when truly urgent. For form validation errors, prefer focus
management to an error summary (see forms/SKILL.md) — do not use role="alert"
on every field error.
Serious: aria-atomic — Announce the Whole Region or Just the Change
aria-atomic="true" → announces the entire region content on any change
aria-atomic="false" → announces only the changed node (default)
For most status messages, aria-atomic="true" is correct — you want the
full message read, not just the changed word.
<div role="status" aria-atomic="true" id="filter-status">
Showing 24 of 156 results for "accessible forms"
</div>
For a chat log or news feed where individual items are added, aria-atomic="false"
is correct — you want each new item announced individually.
<div aria-live="polite" aria-atomic="false" id="chat-log">
<p>Alex: Has anyone reviewed the pull request?</p>
</div>
Serious: aria-relevant — What Triggers an Announcement
aria-relevant="additions" → only new nodes (default)
aria-relevant="removals" → only removed nodes
aria-relevant="text" → text changes
aria-relevant="additions removals"→ both additions and removals
aria-relevant="all" → all changes
Rarely override the default. additions (the default) covers most use cases.
aria-relevant="all" announces every DOM change in the region, which is almost
always too verbose. Only use removals when a removal carries meaning (e.g.,
a notification dismissal that should be announced).
Moderate: Visually Hidden Live Regions
Live regions that contain purely AT-facing announcements should be visually
hidden (not display:none — that removes them from the AT tree too):
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Never use aria-hidden="true" on a live region — it removes the region from
the AT tree and silences all announcements.
Moderate: Common Correct Patterns
Form submission feedback
<div role="status" aria-atomic="true"
class="visually-hidden" id="form-feedback"></div>
<script>
form.addEventListener('submit', async (e) => {
e.preventDefault();
const result = await submitForm(e.target);
announce(result.ok ? 'Form submitted successfully.' : 'Submission failed. Check your entries.');
});
</script>
Search results count
<div role="status" aria-atomic="true"
class="visually-hidden" id="search-status"></div>
<script>
function updateResults(count, query) {
announce(`${count} results found for "${query}"`);
}
</script>
Loading state
<div role="status" aria-atomic="true"
class="visually-hidden" id="loading-status"></div>
<script>
function startLoading() {
announce('Loading results…');
}
function doneLoading(count) {
announce(`${count} results loaded.`);
}
</script>
Character count
<textarea id="message" maxlength="280"
aria-describedby="char-count"></textarea>
<div id="char-count" aria-live="polite" aria-atomic="true">
</div>
<script>
const textarea = document.getElementById('message');
const counter = document.getElementById('char-count');
textarea.addEventListener('input', () => {
const remaining = 280 - textarea.value.length;
if (remaining <= 20) {
counter.textContent = `${remaining} characters remaining`;
} else {
counter.textContent = '';
}
});
</script>
Moderate: When NOT to Use Live Regions
Live regions are the wrong tool when focus management is available:
| Situation | Use instead |
|---|
| Form validation errors | Error summary with focus management (see forms/SKILL.md) |
| Dialog opening | Move focus into dialog |
| Page navigation in SPA | Move focus to <h1> or main content; announce page title |
| Accordion expanding | Move focus to expanded content |
| Toast/snackbar notifications | role="status" with polite; keep message brief |
Overusing live regions creates an announcement-heavy experience that exhausts
screen reader users. Prefer focus management where a natural focus destination exists.
Moderate: Framework-Specific Timing Issues
In React, Vue, and Angular, state changes are asynchronous. The live region
may update before the DOM has settled, causing missed or duplicate announcements.
React pattern:
const [status, setStatus] = useState('');
useEffect(() => {
if (status) {
const region = document.getElementById('status-region');
region.textContent = '';
setTimeout(() => { region.textContent = status; }, 50);
}
}, [status]);
return (
<>
<div id="status-region" role="status" aria-atomic="true"
className="visually-hidden" />
{/* rest of component */}
</>
);
Definition of Done Checklist
Key WCAG Criteria
- 4.1.3 Status Messages (AA, WCAG 2.1) — Critical if status updates not announced
References
Standards horizon: WCAG 3.0 is in development. Status message requirements
are expected to be preserved and potentially expanded.
Monitor: https://www.w3.org/TR/wcag-3.0/