aria-live-regions — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited aria-live-regions (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Canonical source:examples/ARIA_LIVE_REGIONS_BEST_PRACTICES.mdinmgifford/ACCESSIBILITY.mdThis 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.
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:
assertive when polite is correct (interrupts the user)Default to `polite`. Reach for `assertive` only when a delay would cause the user to take a wrong action.
| 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 |
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:
aria-live content even when the region is visually hidden — this can cause duplicate announcements if used alongside visible textThe live region element must be present in the DOM before content is inserted into it. This is the most common live region mistake.
<!-- WRONG: injecting the live region dynamically — AT may miss the announcement -->
<script>
const region = document.createElement('div');
region.setAttribute('aria-live', 'polite');
region.textContent = 'Form submitted successfully.';
document.body.appendChild(region); // AT may not pick this up
</script>
<!-- RIGHT: live region in DOM on page load; content injected later -->
<div aria-live="polite" aria-atomic="true" class="visually-hidden" id="status">
<!-- Empty on load; JS inserts content here -->
</div>// Insert content into the pre-existing region
function announce(message) {
const region = document.getElementById('status');
region.textContent = ''; // Clear first
// Brief timeout ensures AT detects the change even when content is the same
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.
polite vs assertivearia-live="polite" → waits for the user to finish their current action
aria-live="assertive" → interrupts immediately, even mid-sentenceUse `polite` for:
Use `assertive` only for:
`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.
role="status" and role="alert" — The ShorthandTwo 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 |
<!-- Status message — polite -->
<div role="status" class="visually-hidden" id="cart-status"></div>
<!-- Alert — assertive, use sparingly -->
<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.
aria-atomic — Announce the Whole Region or Just the Changearia-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.
<!-- Status: atomic=true ensures the whole message is read -->
<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.
<!-- Chat: atomic=false announces each new message -->
<div aria-live="polite" aria-atomic="false" id="chat-log">
<p>Alex: Has anyone reviewed the pull request?</p>
<!-- New messages appended here -->
</div>aria-relevant — What Triggers an Announcementaria-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 changesRarely 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).
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.
<!-- In DOM on page load -->
<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><div role="status" aria-atomic="true"
class="visually-hidden" id="search-status"></div>
<script>
function updateResults(count, query) {
announce(`${count} results found for "${query}"`);
// Also update the visible count in the UI
}
</script><div role="status" aria-atomic="true"
class="visually-hidden" id="loading-status"></div>
<script>
function startLoading() {
announce('Loading results…');
// Also show visible spinner
}
function doneLoading(count) {
announce(`${count} results loaded.`);
// Hide spinner
}
</script><textarea id="message" maxlength="280"
aria-describedby="char-count"></textarea>
<div id="char-count" aria-live="polite" aria-atomic="true">
<!-- Updated by JS -->
</div>
<script>
const textarea = document.getElementById('message');
const counter = document.getElementById('char-count');
textarea.addEventListener('input', () => {
const remaining = 280 - textarea.value.length;
// Only announce at thresholds to avoid constant interruption
if (remaining <= 20) {
counter.textContent = `${remaining} characters remaining`;
} else {
counter.textContent = ''; // Silent outside threshold
}
});
</script>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.
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:
// Use useEffect to announce after render
const [status, setStatus] = useState('');
useEffect(() => {
if (status) {
const region = document.getElementById('status-region');
region.textContent = '';
setTimeout(() => { region.textContent = status; }, 50);
}
}, [status]);
// Region in JSX — present on initial render
return (
<>
<div id="status-region" role="status" aria-atomic="true"
className="visually-hidden" />
{/* rest of component */}
</>
);aria-live="polite" or role="status" used for non-urgent updatesaria-live="assertive" or role="alert" used only for blocking, urgent messagesaria-atomic="true" set on regions where the full message must be readaria-relevant not overridden unless the default is genuinely wrong.visually-hidden CSS, not display:none or aria-hiddenrole="alert" per field)useEffect / $nextTick timing handled correctlyStandards 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/>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.