Skip to the content.

The accessibility tree is what screen readers actually read

The accessibility tree is what screen readers actually read

A screen reader does not read your DOM. The browser builds a second structure from the DOM — the accessibility tree — and that is what assistive technology consumes. Each node in it carries a role (button, link, heading, checkbox), an accessible name (the text that gets announced), and a set of states (checked, expanded, disabled). Once you know this tree exists, the most common accessibility mystery dissolves: a beautifully styled <div> announces nothing not because the reader is broken, but because that div became an accessibility node with role “generic” and no name — there is simply nothing there to say.

The DOM is projected into an accessibility tree of roles, names and states The DOM on the left maps to an accessibility tree on the right: a button element becomes role button with its text as the name; a styled div becomes role generic with no name, announcing nothing. DOM <button>Save</button><div class="btn">Save</div> browser projects accessibility tree role: button · name: "Save" → announced role: generic · name: (none) → silent
Same visible text, two tree nodes. The button carries a role and a name and is announced; the styled div is generic and nameless, so the reader has nothing to say.

Role, name, state — the three things a node carries

Everything a screen reader announces comes from these three properties. A native element fills them in automatically; a <div> leaves them empty. That is why the same visible text is spoken in one case and skipped in the other:

<!-- role=button, name="Delete" — announced as "Delete, button" -->
<button>Delete</button>

<!-- role=generic, name=none — the reader says nothing useful -->
<div class="button" onclick="del()">Delete</div>

The fix is either the native element (best) or explicitly supplying what the tree needs: role="button", a name, and — separately — the keyboard behaviour, because the tree carries semantics, not behaviour.

Naming: where the accessible name comes from

The accessible name is computed by a specific algorithm, and knowing its order saves a lot of guessing. It prefers aria-labelledby, then aria-label, then the element’s own text content, then things like a <label> or alt. So an icon-only button — no text content — is nameless unless you give it one:

<!-- an icon button with no text: silent until you name it -->
<button aria-label="Close dialog"><svg aria-hidden="true"></svg></button>

Note aria-hidden on the icon: it removes the decorative SVG from the tree so it does not clutter the name with “graphic.”

Inspect the tree, don’t guess

The practical upgrade is to stop imagining what a screen reader hears and look. Every browser’s dev tools expose the accessibility tree, showing each element’s computed role, name, and states — so you can see that your custom checkbox has no “checked” state, or that your icon button is nameless, without launching a screen reader:

DevTools → Elements → Accessibility pane
  <button>  role: button   name: "Close dialog"   states: focusable
  <div.card> role: generic  name: ""               ← the tell: nothing to announce

The accessibility tree is the model that makes ARIA make sense: aria-* attributes are precisely the knobs for setting a node’s role, name, and states when the DOM element does not set them for you. Build with semantic elements so the tree is populated for free, name your icon-only controls, hide decorative graphics, and verify by reading the tree rather than hoping. The accessible-combobox exercise is where this becomes unavoidable — a custom widget has no native role or state, so you compose its accessibility-tree node by hand and check it in the pane.