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.
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)
Level
Meaning
Critical
Dynamic content change conveys no information to AT; user cannot complete task
Serious
assertive/role="alert" used for non-urgent updates, interrupting mid-sentence; live region not exposed before the update it must report
Moderate
Status updates absent; success/failure not announced; announcements too verbose
Minor
Redundant 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:
Situation
Primary mechanism
A button expands/collapses content
Keep focus on the button; update aria-expanded
A tab changes panels
Use the tabs pattern and its selected state
A modal dialog opens
Move focus into the dialog
An SPA changes route
Update the document title; move focus to a logical heading
A form reveals several errors
Error summary + focus management; associate each error with its field
Save/cart/filter/search completes
Concise visible role="status" message
An important time-sensitive condition occurs
Visible role="alert" message
A condition requires an immediate decision
Alert dialog; move focus into it
New entries appended to an ordered history
role="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"
Role
Implicit aria-live
Implicit aria-atomic
Use for
role="status"
polite
true
Advisory info not requiring immediate action
role="alert"
assertive
true
Important, usually time-sensitive information
role="log"
polite
—
Ordered sequences where new info is added (chat, activity log)
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
with full modal dialog keyboard handling instead. Do not use role="alert"
for routine validation, success confirmations, or every field error.
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):
Value
Meaning
off
Default — updates not presented unless focus is within the region
polite
Present at the next graceful opportunity
assertive
Highest 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.
<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.
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.
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:
<button type="button" id="add-to-cart">Add notebook to cart</button>
<p id="cart-status" role="status"></p>
addButton.addEventListener('click', () => {
addProduct('Notebook');
cartStatus.textContent = 'Notebook added to cart. 3 items in cart.';
});
Search results with stale-request handling:
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:
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:
announce the result of an explicit user action before background activity
debounce filter/search announcements triggered by typing
combine related updates into one useful sentence
cancel or ignore stale asynchronous responses
don't announce scroll, pointer movement, animation frames, or clock ticks
avoid announcing a loading state when completion is effectively immediate
provide pause/mute controls for persistent streams (chat, sports, monitoring)
avoid simultaneous status and alert messages for the same event
Avoid prefixes like "Notification" or "Status" when the role already
provides that context.
Framework and Component Guidance
The DOM and accessibility tree produced at runtime determine accessibility,
not the framework. Place a stable announcer near the application root when
many components need one; render announcement content through framework
state (not direct node mutation the framework also manages); ensure the
announcer has mounted before a later state change updates it; prevent nested
roots/portals from creating duplicate active announcers; treat hydration,
route transitions, and remounting as timing boundaries that may reset the
region; coalesce state updates so intermediate text isn't announced.
React pattern (mount first, update via effect, no fixed delay needed
if the region is already mounted before state changes):
const [status, setStatus] = useState('');
useEffect(() => {
// Runs after the region has mounted and rendered; no arbitrary timeout needed
}, [status]);
return (
<>
<div id="status-region" role="status" aria-atomic="true" className="visually-hidden">
{status}
</div>
</>
);
A framework utility can coordinate priority, deduplication, and stale-request
handling — it still requires manual testing with supported AT.
Testing
Static inspection: the chosen role matches the message's purpose;
implicit role properties aren't contradicted by explicit ones; the region is
exposed before the update it must report; the region isn't hidden from the
accessibility tree during the update; status text is short and doesn't wrap
a large changing container; live regions aren't nested; duplicate announcers
aren't mounted; aria-busy returns to false after success and failure.
Manual interaction testing (per important flow): start reading content
away from the update; initiate the action with the keyboard; confirm the
message is visible when it should be; confirm a polite message doesn't
needlessly interrupt; confirm an alert is reserved for an important
condition; trigger rapid updates and verify only the useful result is
presented; repeat the same action and verify the second outcome is announced;
trigger overlapping requests and confirm stale results aren't announced;
confirm focus remains logical and isn't moved by a status update.
Automated checks can detect invalid ARIA values, conflicting explicit/
implicit properties, live regions hidden with aria-hidden, nested live
regions, duplicate IDs, and components leaving aria-busy="true" after
tests complete. Add integration tests asserting the final DOM message after
a user action, including error and stale-request paths — a passing DOM
assertion is not proof an AT announced it correctly.
Common Failures
Failure
Correction
Adding a new live-region element and its message in one operation
Expose a stable region, then update it after a later event
Requiring the region to exist on initial page load
Require it exposed before the relevant update, including in mounted components
Clearing and reinserting text after a fixed delay (e.g. 50ms)
Remove the universal timeout; update stable state and test repeated outcomes
Using assertive for ordinary confirmations
Use role="status"
Expecting assertive to guarantee immediate interruption
Treat it as priority; test actual supported combinations
Putting role="alert" on every field error
Use an error summary, field associations, and focus management
Moving focus to content when an accordion opens
Keep focus on the disclosure control; update aria-expanded
Announcing an entire result list
Announce a concise result summary
Hiding an empty region with display: none
Keep the stable region exposed when a later update must be announced
Announcing every character, percentage, or second
Announce meaningful thresholds and completion
Leaving aria-busy="true" after an exception
Reset it in every completion path
Duplicating visible and hidden live messages
Use one status message or a tested non-duplicating design
Definition of Done Checklist
Each message classified as control state, status, alert, log, dialog content, or focused content
Visible information provided wherever practical
role="status" used for advisory updates; role="alert" limited to important/time-sensitive updates
Alert dialog + focus management used when a response is required
Live region exposed before the relevant content update — no reliance on initial-page-load or fixed-delay announcement guarantees
No fixed-delay clear-and-reinsert workaround used
aria-atomic, aria-relevant, aria-busy used only when their behavior matches the update; default aria-relevant understood as additions text
Region never hidden from the accessibility tree during an update
Large containers and interactive controls not used as live regions
Rapid, duplicate, and stale updates reduced or suppressed
Focus management remains correct with announcements enabled or missed
Success, error, loading, repeated-action, and overlapping-request paths tested
Tested: NVDA+Chrome, JAWS+Chrome, VoiceOver+Safari (and NVDA+Firefox if in the support matrix)
Key WCAG Criteria
4.1.3 Status Messages (AA) — Critical if status updates not announced
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/
Load this skill immediately whenever a project contains an ACCESSIBILITY.md file, or whenever you are contributing to the mgifford/ACCESSIBILITY.md repository. Absolutely always read ACCESSIBILITY.md before proposing or writing any changes. Under no circumstances skip this skill when an ACCESSIBILITY.md file is present. This skill governs when and how all other accessibility topic skills must be loaded.
Load this skill whenever the project contains in-page anchor links, skip links, fragment identifiers, or heading links. Under no circumstances omit accessible anchor-link requirements when such links exist. Ensure every anchor link has meaningful text, a reachable target, and a visible focus indicator. Apply these rules when creating or reviewing any in-page navigation.
Audit and fix common accessibility issues in Angular templates and Angular Material components. Use when the user mentions Lighthouse, axe, screen readers, keyboard navigation, ARIA, asks to fix a11y issues in Angular HTML templates, or after any feature, bug fix, or refactor that changed Angular templates.
Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.
Creates a new Angular app using the Angular CLI. This skill should be used whenever a user wants to create a new Angular application and contains important guidelines for how to effectively create a modern Angular application.