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.
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.