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.

Two ways to create a live region Comparison of a region rendered in the initial HTML against one created dynamically at the moment of the announcement. Two ways to create a live region✓ Present at loadRendered in the static HTML, emptyRegistered before any content changesAnnounces reliably on all four major readersOne region reused for every message✗ Created on demandInserted and populated in the same tickFrequently missed, most reliably on VoiceOverProduces intermittent bugs that look liketiming luckA new region per message multiplies the problem
One of these works on every reader; the other works on some, sometimes.

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);
}
Why a region must exist before its content Timeline contrasting a region present at page load, which is registered and announced, with a region created together with its text, which is missed. Why a region must exist before its contentPage loadempty region insertedAT registers itnow observedLater writeannouncedAnti-patternregion + text together:silenttime →
Assistive technology registers a live region when it is inserted, not when it is written.

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.

Re-announcing identical text Three-step sequence for re-announcing the same message: clear the region, wait one frame, then write the identical text. Re-announcing identical textSame text written againno DOM mutation occursThe reader has nothing to observeClear the region firsttextContent = ""One mutationWrite on the next framethe original text againA second mutation, and an announcement
A region whose text does not change produces no mutation, and no mutation means no announcement.

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.

Permalink to "Related"

Back to ARIA Live Regions for Dynamic Data