Chapter 2: DOM Tree vs Accessibility Tree
What the browser sees vs what assistive tech understands and why that distinction matters
Most front-end developers know about the DOM.
But very few understand the Accessibility Tree, let alone how it’s created, how it works, or how to debug it.
If you’ve ever used a screen reader and thought, “Why isn’t it reading what I expect?”
Or if you’ve added ARIA roles without really knowing why…
This chapter is for you.
Section 2.1: What Is the DOM Tree?
The DOM (Document Object Model) is the browser’s internal representation of your HTML.
It’s a live, dynamic tree of nodes that JavaScript can read, update, and traverse.
When you write this:
<h1>Hello, world</h1>
The browser parses it into this:
Document
└── html
└── body
└── h1 ("Hello, world")
This structure is:
- Built from your source HTML
- Continuously updated by JS
- Used by the rendering engine to paint pixels
- Readable via
document.querySelector()
, etc.
The DOM tree is for the browser. It reflects everything, including elements that are hidden, purely visual, or meaningless to users.
Section 2.2: So What’s the Accessibility Tree?
The Accessibility Tree is a simplified version of the DOM that assistive technologies (like screen readers) use to understand and interact with your page.
Think of it as the semantic version of the DOM.
It’s built by the browser using:
- HTML semantics
- ARIA attributes
- Computed styles (like
display: none
) - Element focusability
It filters out things that don’t matter to assistive tools and highlights roles, names, states, and relationships.
Why Two Trees?
Because screen readers don’t care about layout.
They care about:
- What is this element? (Role)
- What does it say? (Name)
- What state is it in? (Expanded? Required?)
- How is it related to other things? (Described by?)
If the DOM is the full script, the Accessibility Tree is the stage directions for users who don’t see the play.
Section 2.3: Building Blocks of the Accessibility Tree
Each node in the Accessibility Tree has:
Property | Meaning |
---|---|
Role | What it is (e.g. button, heading, link) |
Name | What it says (e.g. “Submit”, “Search”) |
State | Its status (e.g. aria-checked="true" ) |
Relations | How it links to others (e.g. label → input) |
Focus | Whether it’s reachable by keyboard |
Examples
Native button
<button>Send</button>
Accessible node:
- Role:
button
- Name: “Send”
- Focusable: Yes
Custom button with div
<div onclick="send()" role="button" tabindex="0">Send</div>
Accessible node:
- Role:
button
(but only because you faked it) - Name: “Send”
- Focusable: Yes (only because of
tabindex
)
It works, but it’s fragile. Use native elements whenever possible.
Non-accessible element
<span class="hidden">Warning!</span>
If .hidden { display: none; }
, then:
- Role: none
- Name: not read
- It’s completely excluded from the Accessibility Tree
Section 2.4: How the Browser Builds the Tree
Step-by-step:
- Parse the DOM from your HTML
- Remove hidden elements (
display: none
,aria-hidden="true"
) - Infer roles from tags (
<button>
= role: button) - Compute names from:
- Text content
aria-label
- Associated
<label>
elements
- Add states from ARIA (
aria-expanded
,aria-disabled
, etc.) - Link relationships via
aria-describedby
,aria-labelledby
This runs in real-time and updates as your JS modifies the DOM.
Section 2.5: How to Inspect the Accessibility Tree
In Chrome:
- Right-click → Inspect
- Open the Accessibility tab in DevTools
- Select any element
- View:
- Role
- Name
- Keyboard-focusable
- Ignored status
Bonus: Enable full tree with chrome://accessibility
DevTools Example
<button aria-label="Send email">📤</button>
DevTools Accessibility panel shows:
- Role: button
- Name: “Send email”
- Focusable: Yes
Even though there’s no visible text, the button is named.
Section 2.6: Real Mistakes Developers Make
Placeholder instead of label
<input type="text" placeholder="Email" />
Placeholder disappears on focus. Screen readers don’t treat it like a label.
Do this:
<label for="email">Email</label>
<input id="email" type="email" />
Using display: none
instead of hiding visually
.visually-hidden {
display: none;
}
Use:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
clip: rect(0 0 0 0);
overflow: hidden;
white-space: nowrap;
}
This keeps it visible to screen readers but hidden from sighted users.
Not using aria-describedby
<input id="name" />
<p id="name-help">Your full legal name</p>
Add:
<input id="name" aria-describedby="name-help" />
Now screen readers read both input + hint.
Section 2.7: Accessibility APIs Behind the Scenes
The Accessibility Tree is not just DevTools sugar, it’s how the browser communicates with the OS.
It sends this data to:
- Windows: UIA / MSAA
- macOS: NSAccessibility
- Android: AccessibilityNodeInfo
- iOS: UIAccessibility
Which powers:
- Screen readers (NVDA, JAWS, VoiceOver)
- Switch controls
- Braille devices
- Voice navigation systems
If your Accessibility Tree is broken, the entire assistive ecosystem is broken for your users.
Final Checklist
- Every input has a proper
<label>
oraria-label
- Avoid
display: none
unless nothing should access it - Use
aria-describedby
to connect help text - Prefer semantic HTML over ARIA roles
- Inspect your Accessibility Tree in DevTools
- Test your site using a keyboard and screen reader
Resources to Go Deeper
- Accessibility Tree – Chrome Docs
- MDN: Accessibility Tree
- WebAIM: Semantic Structure
- Deque University: ARIA & Forms
Coming Up Next
In Chapter 3, we’ll explore how the browser calculates layout: margins, paddings, borders, collapsing behavior, and real-world debugging of rendering issues using Chrome DevTools.
This is where the pixels hit the screen and where you’ll stop fighting the box model.