Skip to content

The Accessibility Tree

intermediate14 min read

The Tree Screen Readers Actually Read

Here's something that surprises a lot of developers: screen readers don't read the DOM. They read a completely different tree — the accessibility tree. The browser builds this parallel structure from the DOM, strips out everything purely decorative, and exposes what's left to assistive technologies through platform accessibility APIs.

Every time you wonder "why isn't the screen reader announcing this?" or "why is it reading something I didn't expect?" — the answer is almost always in the accessibility tree.

Mental Model

Think of the DOM as the full architectural blueprint of a building — every pipe, wire, and decorative molding. The accessibility tree is the floor plan posted at the entrance — it shows rooms, doors, elevators, and exits. A visitor using the floor plan doesn't need to know about the wiring behind the walls. They need to know: what's this room (role), what's it called (name), is the door open or locked (state), and what floor am I on (value).

How the Browser Builds It

The accessibility tree isn't something you create manually. The browser generates it automatically by walking the DOM and, for each element, computing four properties:

  1. Role — What is this thing? A button, a link, a heading, a list item?
  2. Name — What's it called? The text label that identifies it.
  3. State — What's its current condition? Checked? Expanded? Disabled?
  4. Value — What data does it hold? The current text in an input, the position of a slider.

The browser maps native HTML elements to roles automatically. A <button> gets role button. An <a href> gets role link. An <input type="checkbox"> gets role checkbox with a checked state. You don't need ARIA for any of this — semantic HTML does it for free.

<!-- DOM -->
<nav>
  <ul>
    <li><a href="/home">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

<!-- Accessibility tree (simplified) -->
<!-- navigation                          role: navigation      -->
<!--   list                              role: list            -->
<!--     listitem                        role: listitem        -->
<!--       link "Home"                   role: link, name: Home -->
<!--     listitem                        role: listitem        -->
<!--       link "About"                  role: link, name: About -->

Notice that the <nav> becomes a navigation landmark, <ul> becomes a list, each <li> becomes a listitem, and each <a> becomes a link with its text content as the accessible name. No ARIA attributes needed.

Quiz
A developer writes a div with an onclick handler instead of using a button element. What role does the div get in the accessibility tree?

The Four Properties in Depth

Role: What Is This Thing?

The role tells assistive technology what kind of element this is. Most HTML elements have an implicit role:

HTML ElementImplicit Role
<button>button
<a href="...">link
<input type="text">textbox
<input type="checkbox">checkbox
<select>combobox
<h1> through <h6>heading (with level)
<nav>navigation
<main>main
<img>img
<table>table
<div>, <span>generic (no role)

When you add role="button" to a <div>, you're overriding the implicit role. The accessibility tree now sees it as a button. But this only changes the announcement — you still need to add keyboard handling, focus management, and state management yourself.

Name: What's It Called?

The accessible name is how an element is identified to the user. Browsers compute it using a priority algorithm defined in the Accessible Name and Description Computation spec. The simplified priority:

  1. aria-labelledby (references another element's text)
  2. aria-label (direct string label)
  3. Native label association (<label for="...">, <figcaption>, <caption>, <legend>)
  4. Text content (for elements like buttons, links, headings)
  5. title attribute (last resort — avoid relying on this)
  6. placeholder (for inputs — also a poor label choice)
<!-- Name from text content -->
<button>Save Changes</button>
<!-- Accessible name: "Save Changes" -->

<!-- Name from aria-label -->
<button aria-label="Close dialog">X</button>
<!-- Accessible name: "Close dialog" -->

<!-- Name from associated label -->
<label for="email">Email Address</label>
<input id="email" type="email" />
<!-- Accessible name: "Email Address" -->

<!-- Name from aria-labelledby -->
<h2 id="section-title">Payment Details</h2>
<form aria-labelledby="section-title">...</form>
<!-- Accessible name: "Payment Details" -->

State: What Condition Is It In?

States are dynamic properties that change during interaction:

  • aria-checked — is the checkbox on or off?
  • aria-expanded — is the dropdown open or closed?
  • aria-disabled — is this element usable?
  • aria-selected — is this tab/option chosen?
  • aria-pressed — is this toggle button active?
  • aria-hidden — should assistive tech ignore this entirely?

Native HTML elements update these automatically. A checked <input type="checkbox"> reports checked: true without you doing anything. Custom components need manual state management.

Value: What Data Does It Hold?

The value represents the current data:

  • An <input> has the current text as its value
  • A <progress> has its current percentage
  • A range slider has its current position
  • A <select> has the currently selected option text
Quiz
An image has both alt text and aria-label set. Which one becomes the accessible name?

How CSS Affects the Accessibility Tree

This is where things get tricky — and where most accessibility bugs hide. CSS properties can silently add or remove elements from the accessibility tree.

display: none — Removes from Everything

.hidden { display: none; }

An element with display: none is removed from both the visual layout and the accessibility tree. Screen readers won't see it. It doesn't exist as far as assistive technology is concerned.

visibility: hidden — Also Removes from the Accessibility Tree

.invisible { visibility: hidden; }

Despite the element still occupying space in the layout, visibility: hidden removes it from the accessibility tree. The box is there (it takes up space), but assistive technology can't see it.

opacity: 0 — Keeps It in the Accessibility Tree

.transparent { opacity: 0; }

Here's the one that catches people. opacity: 0 makes the element fully transparent — invisible to sighted users — but the element stays in the accessibility tree. Screen readers will still announce it.

This is actually useful. The classic "visually hidden" pattern (also called "sr-only") uses a combination of CSS properties to hide content visually while keeping it accessible:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

This keeps the element in the accessibility tree and the focus order, while making it invisible to sighted users. Perfect for screen-reader-only labels and skip links.

Quick Reference: CSS Visibility vs Accessibility

CSS PropertyVisible?In Layout?In Accessibility Tree?Focusable?
display: noneNoNoNoNo
visibility: hiddenNoYesNoNo
opacity: 0NoYesYesYes
clip-path: inset(100%)NoDependsYesYes
.sr-only patternNoMinimalYesYes
Quiz
You hide a form error message with visibility: hidden and plan to show it when validation fails. A screen reader user submits the form. What happens with the error message?

aria-hidden: The Manual Override

Sometimes you need explicit control over what enters the accessibility tree. aria-hidden="true" removes an element and all its descendants from the accessibility tree while keeping it visually present.

<!-- Decorative icon next to button text -->
<button>
  <svg aria-hidden="true" focusable="false"><!-- icon SVG --></svg>
  Save
</button>
<!-- Screen reader hears: "Save, button" -->
<!-- Without aria-hidden, it might announce SVG path data as gibberish -->

Common uses:

  • Decorative icons — icons next to text labels that add no information
  • Duplicate content — a visual badge that repeats information already in text
  • Background visuals — animated backgrounds, decorative illustrations
  • Modal backdrops — the content behind an open modal
Common Trap

Never put aria-hidden="true" on an element that contains focusable children (links, buttons, inputs). The element is removed from the accessibility tree, but keyboard focus can still land on the children. This creates a disorienting experience — the screen reader goes silent while the user is clearly focused on something. If you need to hide a section and its focusable children, use the inert attribute instead.

aria-hidden vs display: none

Use display: none when you want to hide something from everyone (sighted users and assistive technology). Use aria-hidden="true" when you want to hide something from assistive technology only while keeping it visually present.

<!-- Hide from everyone -->
<div style="display: none">
  This is not rendered and not in the accessibility tree.
</div>

<!-- Hide from assistive technology only -->
<div aria-hidden="true">
  This is visible on screen but invisible to screen readers.
</div>

<!-- Hide from sighted users only (opposite of aria-hidden) -->
<span class="sr-only">
  This is invisible on screen but announced by screen readers.
</span>

role="presentation" and role="none"

These two are synonyms — role="none" is the modern spelling, role="presentation" is the older one. Both do the same thing: strip the element's semantic role from the accessibility tree, but keep its text content accessible.

<table role="presentation">
  <tr>
    <td>Layout content here</td>
  </tr>
</table>
<!-- The table, tr, and td roles are stripped -->
<!-- Screen reader sees just: "Layout content here" -->
<!-- No "table with 1 row and 1 column" announcement -->

This is useful when you're using an element for layout purposes only. The classic example: layout tables. A <table> used for visual alignment (not data) should get role="presentation" so screen readers don't announce table semantics.

Key difference from aria-hidden: role="presentation" removes the element's role but keeps the content. aria-hidden="true" removes everything — role, name, content, descendants — from the accessibility tree.

<!-- role="presentation" — role stripped, text preserved -->
<h2 role="presentation">Decorative Heading</h2>
<!-- Screen reader sees: "Decorative Heading" (just text, no heading role) -->

<!-- aria-hidden="true" — everything removed -->
<h2 aria-hidden="true">Decorative Heading</h2>
<!-- Screen reader sees: nothing -->
Quiz
A developer uses a table for page layout and adds role='presentation' to the table element. What does a screen reader announce when it reaches the table?

Inspecting the Accessibility Tree in Chrome DevTools

You don't have to guess what the accessibility tree looks like. Chrome DevTools lets you inspect it directly.

Method 1: The Accessibility Pane

  1. Open DevTools (F12 or Cmd+Opt+I)
  2. Select an element in the Elements panel
  3. Open the Accessibility pane in the right sidebar (you might need to click the >> overflow button to find it)
  4. You'll see the computed role, name, and ARIA attributes for the selected element

The Accessibility pane shows you:

  • Computed Properties — the final role, name, and description after all computations
  • ARIA Attributes — any explicit ARIA attributes on the element
  • Source — where the accessible name comes from (aria-label, text content, etc.)

Method 2: Full Accessibility Tree View

  1. Open DevTools
  2. In the Elements panel, click the accessibility icon in the top-right (person silhouette) or use the dropdown to select "Switch to Accessibility Tree view"
  3. Now the entire DOM tree view switches to show the accessibility tree structure

This view is incredibly powerful. You can see:

  • Which elements are present in the accessibility tree
  • Which have been removed (they won't appear at all)
  • The computed role and name for every node
  • The hierarchy as screen readers would navigate it

Method 3: Lighthouse Accessibility Audit

  1. Open DevTools → Lighthouse tab
  2. Check only "Accessibility"
  3. Run the audit

This gives you a scored report of accessibility issues — missing alt text, low contrast, missing labels, invalid ARIA. It won't catch everything (automated testing catches about 30-50% of issues), but it's a solid starting point.

Why Automated Testing Only Catches Half the Issues

Tools like Lighthouse, axe, and WAVE are great at finding structural issues — missing alt text, missing form labels, duplicate IDs, invalid ARIA roles. These are pattern-matching problems with clear right/wrong answers.

But they can't evaluate:

  • Is this alt text actually descriptive? alt="image" passes automated checks but is useless
  • Does the tab order make sense? A logical tab order depends on visual layout context
  • Are live regions too verbose? aria-live="assertive" on a chat feed will interrupt every message
  • Is the heading hierarchy meaningful? Having h1-h6 in order passes automated checks, but the hierarchy might not reflect the actual content structure
  • Can a user complete this workflow with a keyboard? Complex custom widgets need manual testing

This is why accessibility engineering requires both automated scanning AND manual testing with actual screen readers (VoiceOver, NVDA, JAWS).

Elements That Don't Appear in the Accessibility Tree

Not every DOM node gets a slot in the accessibility tree. The browser omits:

  • Elements with display: none or visibility: hidden
  • Elements with aria-hidden="true"
  • Purely decorative elements that add no information (browser heuristics vary)
  • <script>, <style>, and <head> content
  • Elements with role="presentation" or role="none" lose their role (but text content remains)
  • <img> elements with empty alt text (alt="") — this signals the image is decorative

That last one is important. An <img alt=""> is not the same as an <img> with no alt attribute. The empty alt explicitly tells the browser "this image is decorative, skip it." A missing alt attribute makes the browser guess — and it might announce the filename, which is never useful ("banner-final-v3-FIXED.jpg").

Quiz
What is the difference between an image with alt='' (empty string) and an image with no alt attribute at all?

Real-World Patterns

Pattern: Icon Buttons

<!-- BAD: Screen reader says nothing useful -->
<button>
  <svg><!-- arrow icon --></svg>
</button>

<!-- GOOD: aria-label provides the name -->
<button aria-label="Go back">
  <svg aria-hidden="true" focusable="false"><!-- arrow icon --></svg>
</button>

<!-- ALSO GOOD: Visually hidden text -->
<button>
  <svg aria-hidden="true" focusable="false"><!-- arrow icon --></svg>
  <span class="sr-only">Go back</span>
</button>

Pattern: Decorative vs Informative Images

<!-- Decorative: hero background pattern -->
<img src="pattern.svg" alt="" />

<!-- Informative: chart showing data -->
<img src="revenue-chart.png" alt="Revenue chart showing 40% growth from Q1 to Q3 2025" />

<!-- Complex: diagram that needs longer description -->
<figure>
  <img src="architecture.png" alt="System architecture diagram" aria-describedby="arch-desc" />
  <figcaption id="arch-desc">
    Three-tier architecture: React frontend connects to Node.js API layer,
    which queries PostgreSQL database. Redis sits between the API and database
    as a caching layer.
  </figcaption>
</figure>
<body>
  <a href="#main-content" class="sr-only focus:not-sr-only">
    Skip to main content
  </a>
  <nav><!-- long navigation --></nav>
  <main id="main-content">
    <!-- actual content -->
  </main>
</body>

Skip links are invisible until focused (keyboard users pressing Tab). They let users jump past repetitive navigation — essential for keyboard-only users who would otherwise have to tab through 50 nav links on every page.

Key Rules
  1. 1Use semantic HTML first — it gives you correct roles, names, and states for free
  2. 2Every interactive element needs an accessible name — if there's no visible text, use aria-label or visually hidden text
  3. 3display: none and visibility: hidden both remove from the accessibility tree. opacity: 0 does not
  4. 4aria-hidden='true' removes an element AND all descendants from the accessibility tree — never put it on a parent of focusable elements
  5. 5role='presentation' strips the role but keeps text content. aria-hidden strips everything
  6. 6Every img needs an alt attribute — either descriptive text or empty string for decorative images. Never omit it
  7. 7Inspect the accessibility tree in Chrome DevTools — don't guess what assistive tech sees
What developers doWhat they should do
Using a div with onclick instead of a button element
A div has no implicit role, is not keyboard focusable, and won't be announced as interactive by screen readers. A button gives you all of this for free.
Use a native button element for clickable actions
Hiding content with opacity: 0 thinking screen readers won't see it
opacity: 0 only affects visual rendering. The element stays fully present in the accessibility tree and can still receive focus.
Use display: none or visibility: hidden to hide from everyone, or aria-hidden='true' to hide from assistive tech only
Putting aria-hidden='true' on a modal backdrop that contains the close button
aria-hidden removes elements from the accessibility tree but doesn't prevent keyboard focus. Users can tab to hidden children, creating a confusing experience where the screen reader goes silent.
Only apply aria-hidden to truly decorative elements. Use the inert attribute to disable an entire section including focus
Omitting the alt attribute on images instead of using alt='' for decorative ones
A missing alt attribute may cause screen readers to announce the filename or URL. An explicit alt='' tells the browser the image is intentionally decorative.
Always include alt — use descriptive text for informative images, empty string for decorative ones
Using role='presentation' on a data table to suppress table announcements
Screen reader users rely on table semantics (row/column headers, cell navigation) to understand tabular data. Stripping roles from a data table makes the data incomprehensible.
Only use role='presentation' on layout tables. Data tables need their semantic roles for screen reader navigation