DOM and events · 9 / 10
lesson 9

Accessibility from JavaScript

ARIA live regions, announcing changes, when to update aria-* attributes, and what not to ARIA.

~ 14 min read·lesson 9 of 10
0 / 10

A screen reader is a piece of software that reads the page out loud and lets the user navigate it with the keyboard. It looks at the DOM and turns it into speech. When you change the DOM with JavaScript, you are also changing what the screen reader reads — and sometimes you need to tell it that something changed at all.

This lesson is about the small set of ARIA features that actually come up when you write JavaScript. ARIA is huge; most of it is about authoring custom widgets, which is rare and out of scope here. The everyday concerns — announcing a saved confirmation, marking a button as expanded, hiding a decorative spinner — fit on one page.

The first rule of ARIA

Before we add anything: the first rule of ARIA is "don't use ARIA". Whenever you can use a built-in element with the right semantics — <button> instead of <div role="button">, <a> instead of <span role="link">, a <dialog> instead of a custom modal — do that. The native element gets focus, keyboard, and screen-reader behavior for free, and you cannot accidentally turn it off.

ARIA is a patch you apply when you cannot reach for a native element. Patches are necessary sometimes, but each one is also a place where you might lie to assistive tech. A <div role="button"> that does not handle Enter keypresses says it is a button to the screen reader and fails to act like one — that is worse than having no button at all.

A useful picture: ARIA is a set of stickers you can put on an element to say "treat this differently". The browser already came with the right stickers on <button>, <input>, <a>, and friends. You only add stickers when the element you used does not already have the right ones — and the moment you add one, you are responsible for matching its behavior.

Watch out

ARIA does not change behavior. role="button" on a div tells the screen reader "this is a button". It does not make Enter click it, does not put it in the tab order, does not change its keyboard behavior. You have to wire all of that yourself — that is why a real <button> is almost always the right call.

ARIA live regions

When your code changes part of the page — a saved-confirmation, a search-result count, an inline error — sighted users see it immediately. Screen-reader users do not, by default. The screen reader's user is somewhere else in the page and has no reason to look back at the part you just changed.

A live region is a part of the page you have marked as "if I change, announce the change". You set that with the aria-live attribute (or with one of a handful of native roles like role="status" and role="alert").

status.html
<div id="status" role="status" aria-live="polite"></div>
status.js
function showSaved() {
document.querySelector('#status').textContent = 'Changes saved.';
// a screen reader announces "Changes saved" without focus moving
}

The two values you actually use:

  • aria-live="polite" — announce when the screen reader is idle. Use for non-urgent confirmations: "Saved", "Updated", "5 results found".
  • aria-live="assertive" — announce immediately, interrupting whatever the screen reader is saying. Use sparingly, for actual urgency: "Connection lost", "Validation error on Email".

There are also two shortcut roles. role="status" implies polite; role="alert" implies assertive. They behave the same as the equivalent aria-live, with a small extra: assistive tech tends to handle them with slightly better defaults. If your live region matches one of those semantics, prefer the role.

A common mistake: marking the live region only after the change. If the element did not have aria-live (or role="status") when the page loaded, screen readers may not watch it. The right shape is: have the empty live region in your HTML up front, then update its text from JS.

Tip

Don't keep adding more text to a live region. Each update is announced once. If you want to re-announce, replace the whole text — not append. And always replace with textContent, not innerHTML.

check your understanding
A user submits a form. JavaScript shows a toast at the corner of the screen that says "Saved". A sighted user notices it immediately. A screen-reader user does not. What is missing?

Updating aria-* state

A handful of aria-* attributes describe the state of an interactive element. When the state changes, you update the attribute. The screen reader reads the updated state next time the element is announced.

The ones you will actually use:

  • aria-expanded="true|false" — for a toggle that opens and closes related content. A disclosure button, a menu trigger, an accordion header.
  • aria-pressed="true|false" — for a toggle button that has on/off state. A "bold" button in a toolbar.
  • aria-selected="true|false" — for one selected option among siblings. A tab in a tab list, an item in a custom listbox.
  • aria-disabled="true|false" — for an element that looks disabled but should still be focusable (so the user can read its label). Prefer the real disabled attribute on form controls; reach for aria-disabled only on custom widgets.
toggle.js
const toggle = document.querySelector('#menu-toggle');
const menu = document.querySelector('#menu');

toggle.addEventListener('click', () => {
const open = menu.hidden === false;
if (open) {
  menu.hidden = true;
  toggle.setAttribute('aria-expanded', 'false');
} else {
  menu.hidden = false;
  toggle.setAttribute('aria-expanded', 'true');
}
});

When the menu opens, the toggle reports aria-expanded="true". When it closes, false. A screen-reader user knows whether they are about to expand or collapse, and the change is announced when the focus is on the toggle.

check your understanding
You build an accordion. Each panel has a button that opens or closes it. Which attribute on the button should update as the panel toggles?

aria-hidden vs hidden vs display

Three different ways to hide a thing, three different effects on the screen reader.

  • element.hidden = true (or the hidden attribute) — hides from sighted users and removes from the accessibility tree. The screen reader does not see it. Equivalent to display: none for accessibility purposes.
  • display: none (CSS) — same: hidden visually and removed from the accessibility tree.
  • visibility: hidden (CSS) — hidden visually, also removed from the accessibility tree.
  • aria-hidden="true" — hides from the accessibility tree only. Sighted users still see it. Use for purely decorative elements (an icon next to a label that is already in text), or for content you want to hide from the screen reader specifically.

The trap to avoid: putting aria-hidden="true" on something that contains an interactive element a keyboard user can still Tab to. The keyboard user lands on a control the screen reader will not announce — they are stuck with no idea what is focused. If you want to take an element out for everyone, use hidden or display: none.

Watch out

Never put aria-hidden="true" on a wrapper that contains a focusable element unless you are sure no keyboard user can reach it. The keyboard tab order does not follow ARIA — focus can land on a control the screen reader has been told to ignore. The result is a silent, untouchable element.

check your understanding
You wrap a sidebar in aria-hidden="true" when it slides off-screen, instead of using hidden. The CSS keeps the sidebar reachable by Tab. What is the bug?

What to take away

The ARIA you need for everyday DOM code reduces to four things.

  1. Use real elements first. <button> over <div role="button">, <dialog> over a custom modal, <input type="…"> over a styled-up text contraption.
  2. Mark up live regions ahead of timerole="status" or aria-live="polite" on the container in your HTML, then change its textContent from JS.
  3. Update state attributes when state changesaria-expanded, aria-pressed, aria-selected. The widget reports its current state to the screen reader.
  4. Never aria-hidden something a user can still Tab to. Hide with hidden or display: none to take an element out of the tab order at the same time.

There is a lot more ARIA in the spec. Most of it is for building custom widgets — comboboxes, treeviews, datagrids — and is genuinely hard to get right. If you find yourself reaching for those, that is a sign the right answer is a library, or a different design.

check your understanding
A "sign up" button shows a small spinner icon while the request is in flight. The icon is purely decorative — the button's text label is unchanged ("Signing up…"). What is the right ARIA on the spinner SVG?
← prevnext lesson →
KeepLearningcertificate
for completing
DOM and events
0 of 10 read