mgifford/accessibility-skills/aria-live-regions

Claude Code and agent skill by mgifford/accessibility-skills

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

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. Never use a fixed-delay clear-and-reinsert workaround — no standard guarantees it works.

Install

npx vskill@latest install mgifford/accessibility-skills/aria-live-regions

SKILL.md

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

Live regions make dynamic status information available to screen reader users without moving keyboard focus — they are useful for concise updates (a result count, a saved confirmation, progress information). Live regions are not a general notification system. They do not replace visible content, native HTML, focus management, or a control's programmatic state. Poorly timed or excessive announcements can be missed, duplicated, reordered, or disruptive.

Prefer visible status messages — they also help magnification users, cognitive/learning-disabled users, and anyone who wouldn't otherwise notice a change elsewhere on the page. Use native HTML and programmatic control state (e.g., aria-expanded) before reaching for a live region.


Severity Scale (this skill)

LevelMeaning
CriticalDynamic content change conveys no information to AT; user cannot complete task
Seriousassertive/role="alert" used for non-urgent updates, interrupting mid-sentence; live region not exposed before the update it must report
ModerateStatus updates absent; success/failure not announced; announcements too verbose
MinorRedundant announcements; live region content duplicates visible text unnecessarily

What WCAG 4.1.3 Actually Requires

WCAG 4.1.3 requires existing status messages to be programmatically determinable so AT can present them without moving focus. It does not require creating a new status message for every dynamic change — an updated result list is content; a message like "18 results found" is the status message. A dialog receiving focus is a change of context, not a status message.

Choose the mechanism by situation, not by default reflex:

SituationPrimary mechanism
A button expands/collapses contentKeep focus on the button; update aria-expanded
A tab changes panelsUse the tabs pattern and its selected state
A modal dialog opensMove focus into the dialog
An SPA changes routeUpdate the document title; move focus to a logical heading
A form reveals several errorsError summary + focus management; associate each error with its field
Save/cart/filter/search completesConcise visible role="status" message
An important time-sensitive condition occursVisible role="alert" message
A condition requires an immediate decisionAlert dialog; move focus into it
New entries appended to an ordered historyrole="log" when that semantic matches

Live regions can supplement focus management — they should not announce a focus change that already provides the same information.


Critical: role="status", role="alert", and role="log"

RoleImplicit aria-liveImplicit aria-atomicUse for
role="status"politetrueAdvisory info not requiring immediate action
role="alert"assertivetrueImportant, usually time-sensitive information
role="log"polite—Ordered sequences where new info is added (chat, activity log)
html
<p id="cart-status" role="status"></p>
<p id="connection-alert" role="alert"></p>

Do not add redundant explicit aria-live/aria-atomic values on these roles unless a verified compatibility need justifies it. An alert does not receive focus and should not require a response to dismiss — if the user must stop and respond, use the [ARIA APG alert dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/) with full modal dialog keyboard handling instead. Do not use role="alert" for routine validation, success confirmations, or every field error.

html
<div role="log" aria-relevant="additions" aria-atomic="false">
<p>Jordan uploaded quarterly-report.pdf.</p>
</div>

role="log" needs an accessible name (via a heading + aria-labelledby) and each entry should be understandable on its own.

`aria-live` bare values (use only when no live-region role fits):

ValueMeaning
offDefault — updates not presented unless focus is within the region
politePresent at the next graceful opportunity
assertiveHighest priority — use only when interruption is imperative

assertive indicates priority but does not guarantee any specific AT will interrupt speech immediately — actual behavior depends on product, settings, and other queued output. Using `assertive`/`role="alert"` for non-urgent updates is Serious.


Serious: aria-atomic, aria-relevant, and aria-busy

`aria-atomic` (default false): use true when the complete phrase provides necessary context ("3 items in cart"); use false when each addition is independently meaningful (a new chat log entry). status/alert already default to true. When true, the region's accessible name/label may also be presented — test the resulting full phrase, not just the changed text node.

html
<div role="status" aria-atomic="true" id="filter-status">
Showing 24 of 156 results for "accessible forms"
</div>

`aria-relevant` — its default is `additions text`, not just additions. Values: additions, removals, text, all. These are suggestions to AT, not delivery guarantees. Use removals/all sparingly — don't rely on removal announcements for critical information.

`aria-busy` — set true while a region receives a batch of related updates, then reset to false when complete. AT may defer processing until the region is no longer busy.

js
results.setAttribute('aria-busy', 'true');
renderResults(items);
results.setAttribute('aria-busy', 'false');

Always reset `aria-busy`, including after an error — leaving it true after an exception is a common failure. Don't use aria-busy as the only loading indication; provide visible status too.


Critical: Update Timing — No Fixed-Delay Workarounds

Live-region behavior depends on accessibility APIs observing the region before the relevant content change — adding a live region and its completed message in the same DOM operation may be missed. The useful rule: expose a stable live region first, then update its content in response to a later event. The region does not need to exist on initial page load — in a component/SPA it can mount before a later update, as long as it's mounted before that update happens. If users must understand information on initial load, put it visibly in the reading order instead of relying on a live region.

No standard defines a reliable 50ms, 100ms, or any other fixed delay for live regions. Clearing a region and reinserting the same text after a timeout can produce missed, duplicate, stale, or reordered output — do not use this pattern. Instead:

  • keep a stable announcement node
  • update it after the component has mounted
  • let the framework render the message from state
  • announce only the final meaningful result of a rapid sequence
  • suppress responses from stale asynchronous requests
  • use a tested application-announcer abstraction when many components report status

role="alert" has special processing in many implementations, so dynamically rendered alert content is often announced — this does not make alert equivalent to adding aria-live="assertive" to any new element, and does not make initial-page-load alerts reliable.


Serious: Visibility and Content Scope

Use a visually hidden live region only when: equivalent visible information already exists and a nonvisual supplement is genuinely needed, or a concise announcement is needed to describe a complex visible update. Do not duplicate the same message in both a visible and a separate hidden live region — that causes repeated announcements.

css
.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 apply `hidden`, `display: none`, `visibility: hidden`, or `aria-hidden="true"` to a live region while it's expected to announce — avoid rules like .status:empty { display: none; } when the empty region must stay exposed before its next update.

Keep the live region narrow — announce the short status, not an entire results container, form, table, or page section. Avoid nested live regions and avoid a role="alert" inside another live region. Interactive controls generally don't belong inside a live region; if a toast contains an action, keep it persistent long enough to find and operate, with its own focus strategy.


Implementation Patterns

Visible status after an action:

html
<button type="button" id="add-to-cart">Add notebook to cart</button>
<p id="cart-status" role="status"></p>
js
addButton.addEventListener('click', () => {
addProduct('Notebook');
cartStatus.textContent = 'Notebook added to cart. 3 items in cart.';
});

Search results with stale-request handling:

js
let requestSequence = 0;
searchForm.addEventListener('submit', async (event) => {
event.preventDefault();
const sequence = ++requestSequence;
results.setAttribute('aria-busy', 'true');
resultStatus.textContent = `Searching for ${query}.`;
try {
const items = await searchProducts(query);
if (sequence !== requestSequence) return; // stale response, ignore
renderSearchResults(results, items);
resultStatus.textContent = `${items.length} results found for ${query}.`;
} catch (error) {
if (sequence === requestSequence) {
resultStatus.textContent = 'Search could not be completed. Try again.';
}
} finally {
if (sequence === requestSequence) results.setAttribute('aria-busy', 'false');
}
});

Escape/render untrusted result data safely. Consider delaying the visible "Searching" message so fast operations don't announce both loading and completion in immediate succession.

Character count — announce only at thresholds, not every keystroke:

js
const thresholds = new Set([100, 50, 20, 10, 0]);
let lastAnnounced = null;
summary.addEventListener('input', () => {
const remaining = summary.maxLength - summary.value.length;
remainingCount.textContent = remaining; // visible count updates continuously
if (thresholds.has(remaining) && remaining !== lastAnnounced) {
countStatus.textContent = `${remaining} characters remaining.`; // hidden status, threshold only
lastAnnounced = remaining;
}
});

Form errors: show a visible summary and move focus to it — this is a focus-management pattern, a live region is not required for the summary itself. Do not add role="alert" to every field error while the user is typing; a concise nearby role="status" may suit a single async field check if it doesn't duplicate what focus already presented.

Progress: use native <progress>; announce meaningful milestones or completion, not every percentage change.

Session timeout requiring a response: use an alert dialog (role="alertdialog", aria-modal="true", accessible name/description, initial focus inside, keyboard containment, focus returned on close) — an assertive announcement alone does not make a timed interaction accessible. Follow WCAG 2.2.1 Timing Adjustable for the timeout itself.


Moderate: Managing Multiple Updates

Rapid live-region updates can overwhelm the speech queue and hide the outcome the user needs:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/mgifford/accessibility-skills/aria-live-regions/badge)](https://verified-skill.com/skills/mgifford/accessibility-skills/aria-live-regions)

Something wrong with this skill or its page? Tell us.