The exact problem: a form uses role="alert" for every message it announces, so a reader is interrupted mid-word by a character counter — or uses role="status" for a submit failure and the announcement is queued behind something else and never heard.
Context and Prerequisites
This is a narrower decision inside ARIA live regions for form errors, which covers the regions themselves. Here we are only choosing between the two roles a form realistically needs, and deciding how many regions to have.
The Rule
Assertive interrupts. Polite waits. Everything else follows from asking one question: is this message worth cutting someone off mid-sentence?
Almost nothing in a form is. A submit that failed is; a field that resolved is not. A session about to expire is; a saved draft is not.
/**
* Two regions, created once, reused for the life of the form.
* Splitting by urgency rather than by feature is what stops a character counter
* from ever being able to interrupt a submit failure.
*/
const politeRegion = document.getElementById('form-status')!; // role="status"
const urgentRegion = document.getElementById('form-alert')!; // role="alert"
type Urgency = 'polite' | 'assertive';
export function announce(text: string, urgency: Urgency = 'polite'): void {
const region = urgency === 'assertive' ? urgentRegion : politeRegion;
// Identical text does not re-announce, because the DOM did not change.
// Clearing first, then writing in the next frame, forces a change the
// screen reader observes — without the zero-width-space hack.
region.textContent = '';
requestAnimationFrame(() => { region.textContent = text; });
}
Step-by-Step Walkthrough
-
Create both regions once, empty, at form mount. A region added to the DOM at the same moment its text appears is often not announced at all — the screen reader had nothing to observe.
-
Never nest them. Two live regions inside one another produce duplicate announcements, in an order that varies by screen reader.
-
Route by urgency, not by feature. One
announce()function with an urgency argument beats a region per component, which is how a counter ends up assertive. -
Clear before writing. Identical text is not a DOM change and does not re-announce. Clear, then write on the next frame.
-
Use
role, notaria-live, where a role exists.role="status"androle="alert"carry the politeness and a landmark meaning;aria-livealone carries only the politeness. -
Prefer a focus move for anything you also navigate to. If focus is moving to the summary, the focus move announces it — an alert region as well announces it twice.
Failure Modes and Edge Cases
1. The region is added at announcement time
The live region must be in the accessibility tree before its content changes. Render both regions empty at mount, and only ever change their text.
2. Announcing on every keystroke
A validation result announced per keystroke makes typing impossible with a screen reader on. Debounce announcements at least as long as the validation itself, and announce the settled result only.
3. Two regions with the same content
A message written into both the field’s message element and an alert region is announced twice, because both are in the tree. Choose the one that fits the moment.
4. Assertive used for reassurance
“Saved” is reassuring in a status region and hostile in an alert region, where it cuts off whatever the reader was reading in order to say nothing they needed.
5. Visually hidden with display: none
A region hidden with display: none or visibility: hidden is removed from the accessibility tree and announces nothing. Use a clip-based visually-hidden utility instead.
The debounce on announcements needs to be longer than the one on validation, and for a different reason:
Verification Checklist
Common Pitfalls
- Creating the region on demand. A live region added to the document at the same moment its text is written is frequently not announced at all, because the screen reader had nothing in the accessibility tree to observe. Render both regions empty at mount and only ever change their text content.
- Hiding the region with
display: none. That removes it from the accessibility tree entirely, so nothing written into it is ever spoken. Use a clip-based visually-hidden utility, which keeps the element in the tree while taking it out of the visual layout. - Putting a region inside a component that unmounts. A region that disappears with the component it belongs to takes any pending announcement with it. Keep both regions at the form root, above anything conditional.
- Reaching for
aria-livewhen a role exists.role="status"androle="alert"carry the politeness setting plus a landmark meaning that some assistive technology exposes for navigation. The bare attribute carries only the politeness. - Announcing progress in the assertive region. Anything that updates continuously — a percentage, a counter, a queue length — will interrupt on every update. If it is worth announcing at all it is worth announcing politely, and usually only at milestones.
Related
- ARIA Live Regions for Form Errors — the full region model
- aria-invalid Timing and Screen Reader Announcements — when the write is allowed to happen
- Building an Accessible Error Summary — why the summary uses focus instead of a region
← ARIA Live Regions for Form Errors
Frequently Asked Questions
Is role="alert" the same as aria-live="assertive"?
Almost. role=“alert” carries an implicit aria-live of assertive and an implicit aria-atomic of true, and it also gives the element the alert role in the accessibility tree, which some assistive technology exposes as a landmark. aria-live=“assertive” gives you only the announcement behaviour. Prefer the role where one fits the meaning, and fall back to the attribute for anything that is not semantically an alert or a status.
How many live regions should a form have?
Two: one polite, one assertive, created once at mount. Splitting by urgency rather than by feature means the categorisation decision is made at the call site, where the context is, instead of being baked into where a component happens to live. More than two regions makes ordering unpredictable, because the announcement order across several regions is not specified.
Why does the same message not announce twice?
Because a live region announces changes, and writing identical text is not a change to the DOM. Clearing the region and writing the text again on the next frame produces two observable mutations, which is enough. Avoid the trick of appending a zero-width space — it works, but it also ends up in the announced string in some screen readers.