Dynamic content and ARIA
Announce dynamic updates
Use restrained live regions for important asynchronous changes without creating repeated or interrupting noise.
Some changes happen without moving focus. The form is being sent. The registration succeeded. The server didn’t answer. A sighted user sees the message appear. A screen reader user, whose focus is still on the Register button, hears nothing unless you tell the browser this text matters.
That’s what a live region does. It’s an element the screen reader watches; when its text changes, the change is spoken. WCAG 4.1.3 Status Messages (Level AA) asks for exactly this for messages that don’t take focus.
One region, present from the start
Put an empty status element in the form before anything happens:
<p id="form-status" role="status"></p>
role="status" is a polite live region: the screen reader finishes what it’s saying, then reads the new text. The element must already be in the DOM when the page loads. Screen readers watch for changes inside existing regions. A region inserted together with its message is often not announced at all.
Now update its text at each stage of the submission:
const status = document.querySelector('#form-status')
async function submitRegistration(formData) {
status.textContent = 'Sending your registration.'
try {
const response = await fetch('/api/register', { method: 'POST', body: formData })
if (!response.ok) throw new Error(response.status)
status.textContent = 'You are registered. Check your inbox for the ticket.'
} catch {
status.textContent = 'We could not reach the server. Your details are still in the form, try again.'
}
}
Three states, three short sentences, one region. Each message says what happened and, on failure, what the user still has.
Errors are not status
Validation errors don’t go in here. They already have a home: the error summary that receives focus. Announcing them twice, once by focus and once by a live region, is noise. Focus handles changes the user must act on now. Live regions handle changes the user should know about while staying where they are.
Count what’s spoken
With VoiceOver on, submit the form three ways and write down every announcement:
Valid data -> "Sending your registration." / "You are registered. Check your inbox for the ticket."
Two errors -> "There are 2 problems with your registration, heading level 2, group"
Network down -> "Sending your registration." / "We could not reach the server. Your details are still in the form, try again."
Two, one, two. If you hear more, something else on the page is live and shouldn’t be.
The whole-app live region
The flawed page had this on the root:
<body aria-live="assertive">
assertive interrupts whatever is being read. On the body, every tooltip, character counter and hover state becomes an interruption. In a one-minute demo it “works”: the success message is spoken. In real use the page never shuts up, and users turn the screen reader’s verbosity down or leave.
Keep live regions small, few and polite. Use role="alert", which is assertive, only for something that must stop the user, like a session about to expire. And be careful with repeating the same string: setting textContent to a value it already holds may not trigger an announcement. Clear it first if the user can hit the same state twice.
Try this on your own form: submit it successfully, with errors, and with the network offline, and count the announcements for each. Then find and remove any live region wider than a single message.
Lesson completed