Accessibility · ARIA Practice
How Live Regions Turn DOM Mutations Into Speech
Live regions do not announce anything themselves; they mark a subtree as interesting so that mutations inside it are queued, and politeness, atomicity, and relevance decide what is spoken.
The single most common live-region bug is a region that is created at the same moment it is filled. An author writes the announcement text into a container, adds aria-live="polite", and hears nothing; adds a setTimeout of a few hundred milliseconds and it works. The delay is not a workaround for a rendering quirk. It compensates for the fact that a live region is a declaration about a subtree that must already be in the accessibility tree before the mutation it is supposed to report. Everything else about live regions follows from that ordering requirement.
A live region is a subscription, not a message
aria-live has three values: off, polite, and assertive. off is the default and means the subtree is not a live region. The other two mark the element as a source of announcements and choose a queue.
The mechanism is the accessibility tree’s mutation stream. Browsers observe DOM changes and, for any change inside a subtree marked live, emit an appropriate accessibility event: a node added, text changed, an element removed. Assistive technology listens for those events on live containers. A node that is not live produces no such event for its content changes; the tree is updated, and nothing is spoken.
That is why timing matters. When a browser builds the tree for a document, it records which elements are live. If an element is inserted into the DOM already carrying aria-live and already containing its text, the insertion produces a single event: the element was added. There is no text change to report, because the text was never there before. A screen reader that has just discovered a new container has no baseline to compare against and typically says nothing. If the container exists first and the text is written into it a moment later, the write is a text-changed event on a known live region, and it is announced.
The reliable construction is therefore two-step: put an empty live container in the document at load, then mutate its contents.
<div id="status" role="status" aria-live="polite" aria-atomic="true"></div>
const status = document.getElementById('status');
// the container is already in the tree; only its text changes
status.textContent = 'Saved';
The same ordering applies to changing the aria-live value itself. Setting aria-live="assertive" on an element and immediately writing to it is one mutation sequence, and some implementations will not have re-registered the element as live before the text arrives. Setting the attribute in one task and writing content in the next is the pattern that survives across engines.
Politeness is a queue, not a volume
polite means the announcement waits until the user is idle, which in practice means until the screen reader has finished whatever it is currently saying. assertive means the announcement interrupts immediately, cancelling speech in flight.
Both values feed the same speech pipeline that produces every other utterance, and the difference is where they enter it. A polite announcement joins the tail of the queue; an assertive one clears the queue and takes the front. Assertive is not “louder” or “more important” in a general sense: it is destructive, because whatever the user was listening to is lost.
The practical rule follows from the destructiveness. assertive belongs to messages that invalidate the current context, such as an error that means the form cannot be submitted or a session that is about to expire. A validation message on the third field of a form is not that: it interrupts the user’s reading of the field they are on. Repeated assertive updates are worse than repeated polite ones, because each one discards the previous one mid-word. A countdown that announces every second is unlistenable at either politeness level; the fix is to announce less, not to announce more quietly.
A third value, rude, appears in some documentation and in the ARIA specification’s historical vocabulary. It is not part of the current value set for aria-live in WAI-ARIA 1.2, and writing it yields no mapping. Implementations that accepted it treated it as assertive.
Atomicity decides the unit of speech
By default, a live region announces the changed node, not the whole region. aria-atomic="false" is the default, and it means that if a container holds “Uploading” and a progress figure, and only the figure changes, the reader speaks the figure alone. aria-atomic="true" makes the reader treat the region as a single unit and speak its entire text content on any change.
The trade is context against repetition. A status line reading “3 of 12 files uploaded” changes on every file; with atomicity off, the user hears “4”, “5”, “6” with no idea what the numbers mean. With atomicity on, they hear the whole sentence each time, which is comprehensible and verbose. The middle path is to keep the message short enough that full repetition is tolerable, and to avoid regions whose text grows without bound.
Atomicity also interacts with how the mutation is performed. Replacing the text of a child with textContent produces one text-change event on that child. Rebuilding the whole region’s innerHTML produces a children-changed event, and some readers will then announce the whole region regardless of the atomic value, because a subtree replacement is not a text edit. Writing the smallest node that changed is the more predictable operation.
Relevance filters which mutation types count
aria-relevant is a space-separated list controlling which mutation kinds produce announcements. The tokens are additions, removals, text, and all. The default in the specification is additions text, which is why a region whose contents are replaced wholesale usually speaks but a region whose contents are deleted usually does not.
The attribute exists because a chat log and a status message need opposite behaviour. A log wants additions, so new messages are announced and old ones scrolling out of a bounded list are not. A status message wants text, so a change to an existing string is announced but a container being emptied is not. all is rarely right: it announces removal of the very content the user just heard, and in a log that prunes its oldest entries the removal announcement arrives as noise after every addition.
There is a known limitation worth stating plainly. aria-relevant support varies by screen reader and platform, and several widely used combinations honour additions and text while treating removals as advisory. A design that depends on removal announcements for correctness will behave differently across users. Designing so that the important information arrives as an addition, rather than as the absence of something, removes the dependency.
Ownership, and why one state change gets one channel
A state change should produce exactly one announcement. When it produces two, the cause is usually two mechanisms firing for the same fact: a live region announcing “Item deleted” while focus moves to the next item and the reader announces that item’s name as well. Both are legitimate, and together they are a stutter.
The rule that prevents this is to assign each kind of change to one channel. Changes that are the direct result of the user’s own action are usually best reported by moving focus, because focus also carries the user’s position; changes that arrive without user action have no focus to move to and belong in a live region. A delete triggered by pressing a Delete key is a user action, so focus movement to the next row and a row announcement is sufficient. A delete triggered by a background sync is not, so it needs the region.
The same reasoning applies to the region’s own content. If the region says “Saved” and a separate toast says “Saved”, the user hears it twice, and the second instance makes the first look unreliable. One region per message, one writer per region. This is the same single-writer discipline that keeps ARIA state honest, described in mirroring widget state into ARIA attributes.
Where the model breaks
Four failures account for most live-region defects.
The region is created and filled in the same operation, as above, so nothing is announced. The fix is a container that exists before the message.
The region is hidden. A live region inside a subtree with display: none, visibility: hidden, or aria-hidden="true" is not in the accessibility tree, so it emits no events. The common variant is a visually hidden container, which is fine because clipping a box does not remove it from the tree, versus a display: none one, which is not. Toggling visibility to show the message and writing the text in the same task reproduces the creation problem.
The region is too chatty. A progress region updated on every animation frame generates far more events than the speech queue can drain, and the reader either drops them or falls behind permanently. Throttling to whole percentages, or announcing milestones only, is the only fix that scales; no politeness setting compensates for an event rate above the speech rate.
The region is announced while the user is elsewhere. An assertive message about a background task interrupts a user who is reading a form, and the interruption is disorienting because nothing in the current context changed. Scoping regions to the part of the interface they describe, and reserving assertive for changes that affect the user’s current task, is the design constraint the politeness attribute cannot enforce by itself. The roles that make a region’s purpose legible without an explicit aria-live are covered in implicit live roles and when to prefer them.