Creating Live Regions Before Content Changes
Permalink to "Creating Live Regions Before Content Changes"The most common live-region bug is not a wrong attribute — it is a region that did not exist when the browser needed to register it. A region created and populated in the same tick is frequently treated as initial content rather than a change, so nothing is announced. The symptom is maddening: the first message of a session is silent, everything afterwards works, and the behaviour differs between readers and between runs.
This page covers the fix, which is unglamorous and absolute: render the region empty at page load and write into it later. It is a detail of ARIA live regions for dynamic data, and it underpins every announcement pattern on this site.
Spec reference
Permalink to "Spec reference"ARIA describes a live region as a region whose content may change as a result of an event, where those changes should be presented to the user. The specification is deliberately silent about when user agents must begin observing a region, and implementations differ: some observe from insertion, some from the next paint, and all of them treat the content present at registration as the starting state rather than as a change.
That is the whole mechanism behind the bug. If the region and its text arrive together, the text is the starting state.
role="status" carries an implicit aria-live="polite" and aria-atomic="true"; role="alert" carries aria-live="assertive" and aria-atomic="true". Using the roles rather than the raw attributes is preferable because the role also gives the region a landmark-like identity that some readers expose in their element lists.
When to use — and when not to
Permalink to "When to use — and when not to"Render the regions in your application shell — the layout that wraps every page — so they exist from the first paint and survive route changes. Two elements, both empty, both visually hidden.
Do not create a region per message. Toast systems frequently do this, appending a new live element per notification, and it produces exactly the failure this page describes plus a growing pile of stale regions.
Do not move a live region in the DOM. Moving it can re-register it, and some readers then treat everything inside as new. If a message needs to appear in a different place visually, position it with CSS.
Annotated code example
Permalink to "Annotated code example"<!-- In the app shell, rendered on every page, before any content -->
<!-- SC 4.1.3: both regions exist and are empty from first paint -->
<div class="visually-hidden">
<p id="app-status" role="status"></p>
<p id="app-alert" role="alert"></p>
</div>
// A single announcer everything else calls — the regions are never re-created
const status = document.getElementById('app-status');
const alertRegion = document.getElementById('app-alert');
export function announce(message, { assertive = false } = {}) {
const region = assertive ? alertRegion : status;
// Clearing guarantees a mutation even when the text is unchanged
region.textContent = '';
requestAnimationFrame(() => {
region.textContent = message;
});
}
// What NOT to do — the region and its text arrive together
function badAnnounce(message) {
const el = document.createElement('div');
el.setAttribute('role', 'status');
el.textContent = message; // registered WITH content — often silent
document.body.appendChild(el);
}
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Assistive technology | Region present at load | Region created with content |
|---|---|---|
| NVDA + Firefox | Announces every write | Usually announces, occasionally misses the first |
| JAWS + Chrome | Announces every write | Intermittent — depends on timing relative to the render |
| VoiceOver + Safari | Announces after a short delay | Frequently silent |
| TalkBack + Chrome | Announces every write | Intermittent |
The pattern in that table is why the bug survives review: it works on the reader the developer tested with, and fails on the one the user has.
Integration context
Permalink to "Integration context"A single shared announcer is the practical consequence. Every feature on this site that announces — sorting, filtering, pagination, selection — should call the same function rather than owning its own region. That guarantees the regions exist, guarantees there are only two of them, and puts the clear-then-write timing in one place.
In frameworks, mount the announcer at the root and expose it through context, a store or a service. Angular ships one already: the CDK’s LiveAnnouncer follows exactly this pattern.
Gotchas
Permalink to "Gotchas"The region is inside a component that unmounts. A status region inside the table component disappears when the table is replaced, taking the pending message with it. Keep regions in the shell.
The region is inside an aria-busy subtree. Every message is suppressed. Keep regions outside whatever you mark busy.
The region is display: none. Hidden regions are not announced. Use a visually-hidden technique that keeps the element rendered — absolute positioning with a 1 pixel clip.
Two writes in one frame. The second overwrites the first before the reader observes it. Queue messages if that is possible in your application.
Proving the region works
Permalink to "Proving the region works"There is a two-minute check that settles whether a region is registered, and it does not need a screen reader.
Load the page and, before any interaction, confirm in DevTools that both regions exist in the DOM and are empty. If either is missing, the bug is present regardless of what the code appears to do — a region rendered conditionally on the first message is a region created with its content.
Then write to the region from the console twice with the same string, using the clear-then-write helper, and watch the DOM mutation panel. Two mutations per call is correct: one clearing, one writing. A single mutation means the clear was optimised away by the framework, which is the reason an identical message sometimes fails to re-announce.
Finish with a real screen reader for the first message only. That is the message that reveals a registration problem, and it is the one automated tooling cannot check for you.
FAQ
Permalink to "FAQ"Why is my live region silent the first time and fine afterwards?
Because it was created and populated in the same tick. Assistive technology registers a live region when it enters the accessibility tree; content that arrives in the same update is often part of the initial state rather than a change, so there is nothing to announce. Once the region exists, subsequent writes are changes and are announced normally. Render the region empty in the initial HTML and the first message behaves like every other one.
How many live regions should a page have?
Usually two: one polite status region and one assertive alert region. A third, an aria-live=“off” log, is worth adding when a feed needs a readable history that is never announced. More than that and messages start racing each other, and the reader drops whichever loses.
Does clearing the region before writing cause a double announcement?
No. Clearing writes an empty string, and readers do not announce empty content. What it does is guarantee a mutation when the new text is identical to the old, which is the only way to re-announce an unchanged message — a filter that returns the same count twice, for example.
Related
Permalink to "Related"- ARIA live regions for dynamic data — the parent guide and the attribute reference
- Choosing between polite and assertive aria-live regions — which of the two regions to write to
- Screen reader announcement strategies — the copy that goes into them