Accessibility from JavaScript
ARIA live regions, announcing changes, when to update aria-* attributes, and what not to ARIA.
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.
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").
<div id="status" role="status" aria-live="polite"></div>
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.
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.
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 realdisabledattribute on form controls; reach foraria-disabledonly on custom widgets.
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.
Three different ways to hide a thing, three different effects on the screen reader.
element.hidden = true(or thehiddenattribute) — hides from sighted users and removes from the accessibility tree. The screen reader does not see it. Equivalent todisplay: nonefor 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.
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.
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.
- Use real elements first.
<button>over<div role="button">,<dialog>over a custom modal,<input type="…">over a styled-up text contraption. - Mark up live regions ahead of time —
role="status"oraria-live="polite"on the container in your HTML, then change itstextContentfrom JS. - Update state attributes when state changes —
aria-expanded,aria-pressed,aria-selected. The widget reports its current state to the screen reader. - Never
aria-hiddensomething a user can still Tab to. Hide withhiddenordisplay: noneto 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.