Semantics and roles: the accessibility tree does not see your divs
A clickable div has no role, no name, no keyboard path — assistive tech cannot operate it. Native elements ship the contract free; the accname ladder (labelledby beats label beats content beats title) decides the announced name; landmarks and headings are the navigation surface.
The demand letter arrived on a Tuesday: a blind customer had tried to buy a gift card with NVDA and could not, and the law firm that sent it files hundreds of web-accessibility cases a year. The remediation audit cost more than the original frontend build. The first finding took the auditor ninety seconds: the product page had fourteen interactive controls and Tab reached two of them. Everything else was a div with an onClick — styled like a button, behaving like a button for a mouse, and absent from the accessibility tree as a control. No role, so the screen reader announced it as nothing in particular; no accessible name, so even the virtual cursor read only loose text; not focusable, so the keyboard never arrived; no key handling, so Enter would have done nothing anyway. The team had not been careless — they had been building on a component library whose Button primitive was a div, copied forward since 2019, so one wrong element choice metastasized across every funnel. WebAIM’s automated survey of the top million home pages finds detectable WCAG failures on roughly 96% of them, year after year, and the leaders of that failure catalog are exactly these: missing form labels, empty buttons, missing alt text. Fixing most of that catalog does not require learning ARIA. It requires using the elements the platform already ships.
What a button gives you that a div never will
When React commits <button>Save</button>, the browser derives a second tree from the DOM: the accessibility tree, the one screen readers, voice control, and switch devices actually consume. The button node arrives in that tree fully equipped: an implicit role of button, an accessible name computed from its content (“Save”), membership in the sequential focus order, activation on both Enter and Space, correct announcement (“Save, button”), and a visible focus indicator. A div with an onClick arrives as a nameless generic node — present as text geometry, invisible as a control. Voice-control users cannot say “click Save” at it; switch users cannot reach it; keyboard users Tab straight past it.
Rebuilding parity by hand means five obligations at every call site: role="button", tabIndex of 0, an onKeyDown that activates on Enter and on Space (and prevents Space from scrolling the page), disabled semantics via aria-disabled plus actually blocking the handler, and a focus style. Each is individually forgettable, none produces a visible bug for mouse users, and the failure is silent until an audit. This is the first rule of ARIA from the spec authors themselves: if a native element with the semantics you need exists, use it instead of repurposing another element and bolting ARIA on. The corollary is blunter and equally official: no ARIA is better than bad ARIA — role="button" without the keyboard handling is a promise to assistive tech that the element then breaks, which is worse than staying a humble div.
The React-specific angle: JSX adds zero semantics. React renders exactly the elements you wrote, and components hide the element choice — a Button primitive that renders a div ships that decision to every consumer invisibly. Element choice is therefore an API-design decision in a component library, not a per-page styling detail, and it is the cheapest accessibility decision you will ever make: the platform has already implemented and AT-tested the behavior.
A code review proposes fixing a clickable div by adding role='button' and tabIndex={0}. The author says it is now equivalent to a native button. What is still missing?
The accessible name is computed, not guessed
“What does the screen reader call this thing?” has a deterministic answer: the accessible name and description computation, a W3C algorithm that walks a strict precedence ladder per element. Knowing the ladder turns name bugs from folklore into arithmetic. For a given element: aria-labelledby wins if present and non-empty — it collects the referenced elements’ text, in id order, even from hidden nodes. Otherwise aria-label wins — a flat string that replaces content entirely. Otherwise the host language takes over: an associated label element for form controls, alt for images. Otherwise, for roles that allow name from content (button, link, heading — not div, not generic), the subtree text becomes the name. Last resort: title, and for inputs placeholder — both weak, both effectively a lint warning.
Three traps the ladder explains. First: aria-label on a plain div does nothing reliable — generic roles are not name-bearing, so AT may ignore it entirely; the attribute is not a magic caption, it is input to an algorithm that consults the role. Second: because aria-label replaces content, an icon button with visible text “Search” and aria-label="magnifier" is announced as “magnifier” — and a voice-control user saying “click Search” misses, which is why WCAG requires the visible label to appear in the accessible name. Third: aria-labelledby beats everything including itself being weird — it concatenates multiple ids, includes its own element if self-referenced, and reads display:none targets; powerful for composing names (“Delete” + row title), but a stale id reference fails silently to an empty string and the algorithm falls through to the next rung, changing the announcement without any console warning.
A button renders as: button with visible text 'Export', aria-label='Download CSV', and aria-labelledby='panel-title' where panel-title is a heading reading 'Quarterly report'. What does a screen reader announce?
Landmarks and headings are how people navigate
Screen-reader users do not read pages top to bottom; they jump. In WebAIM’s screen-reader user surveys, navigating by headings is consistently the dominant strategy for finding content on a page, with landmarks as the structural complement. That makes the heading outline and the landmark map a primary navigation surface — equal in rank to your visible nav bar. Native sectioning elements ship landmark roles implicitly: header is banner, nav is navigation, main is main, footer is contentinfo. The rules of the surface: exactly one main; label repeated landmarks (aria-label="Primary" on one nav, “Breadcrumbs” on the other — here aria-label is legitimate because nav is a role that names); headings form a real outline — one h1, no level skipping, levels chosen by structure, never by font size. The WebAIM Million data shows how rare this discipline is in the wild: large fractions of home pages lack any landmarks or have broken heading orders, which means a well-structured app is a genuine competitive accessibility feature, not table stakes.
React adds two structural habits. Fragments exist so that “I need a single JSX root” never costs a wrapper div: every needless div is another generic node fattening the accessibility tree, and inside semantic structures it is destructive — a component that wraps its li in a div breaks the required ul-to-li parent chain, and some AT then stops announcing “list, 5 items”. Use Fragment (or key-ed fragments in lists) and keep semantic parent-child chains intact across component boundaries — the DOM does not know your component tree exists, only its output. Second, dangerouslySetInnerHTML forfeits every structural guarantee: injected markup bypasses JSX, so eslint-plugin-jsx-a11y never sees it, heading levels inside CMS-supplied HTML jump arbitrarily, and images arrive without alt. If you must inject (rendered markdown, CMS content), sanitize and audit the output against the same rules, because your tooling just went blind in that subtree.
▸Why this works
Why does ARIA only change claims and never add behavior? Because it was designed as a description layer retrofitted onto arbitrary markup: a contract language with which any element can declare “I am a button” to the accessibility tree. Making the browser also attach button behavior to anything claiming the role would change the runtime semantics of existing pages and create circular expectations (does role=‘checkbox’ now toggle itself?). So the division of labor is strict: native HTML elements bundle claim plus behavior, tested across every browser and screen reader for two decades; ARIA gives you the claim alone and makes the behavior your problem. That asymmetry is the entire economic argument for the first rule of ARIA — with a native element you inherit the expensive half for free.
- 01Walk the accessible name computation for a button with visible text, an aria-label, and an aria-labelledby — and name the three traps the ladder explains.
- 02State the first rule of ARIA, what exactly a div+onClick loses versus a native button, and the two React-specific structural habits.
The browser derives the accessibility tree from the DOM, and that tree only carries what semantics put there. A native button enters it with role, computed name, focus-order membership, Enter and Space activation, and disabled semantics — a clickable div enters as a nameless generic node that screen readers, voice control, and switch access cannot operate. Recreating the contract by hand takes five per-call-site obligations that fail silently for mouse users, which is why the first rule of ARIA says to use the native element when one exists, and why a role claim without its behavior is worse than no claim. Accessible names are computed by a strict per-element ladder — aria-labelledby, then aria-label, then host-language mechanisms, then content for name-bearing roles, then title — first non-empty rung wins, which makes name bugs deterministic: labels on generics do nothing, labels that replace visible text break voice control, stale labelledby ids fail silently to the next rung. Landmarks and the heading outline are the page’s actual navigation surface for screen-reader users — heading jumps dominate in WebAIM’s user surveys, while the WebAIM Million shows roughly 96% of home pages failing automated WCAG checks, led by missing labels and empty buttons. React’s role in all of this is structural: JSX adds no semantics, component primitives hide element choices and replicate them at scale, Fragments keep wrapper divs from breaking semantic parent chains, and dangerouslySetInnerHTML opts a subtree out of every guarantee your lint and your JSX provide. Now when you encounter a component library or code review proposing a styled div as an interactive control, you know exactly what is missing and where to look for it in the name-computation ladder.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.