Ever had that moment where you build a shiny web component with Shadow DOM, test it in your browser, and everything looks perfect visually , but when you use a screen reader, your component feels like a black box? The screen reader either skips it entirely or reads confusing information.
I hit this exact wall recently. My custom dropdown was gorgeous, encapsulated, and well-structured. But accessibility testing with VoiceOver and NVDA revealed a frustrating truth: the accessibility tree didn’t expose my component’s internals correctly. Why? Shadow DOM.
Let me walk you through what’s really going on under the hood with the accessibility tree and Shadow DOM, why your screen reader might be getting lost in the shadows, and practical steps I took to fix it.
The accessibility tree: your UI’s secret map
You might be familiar with the DOM tree , the nested structure of HTML elements your browser renders. But assistive technologies don’t read the DOM directly. Instead, browsers build a parallel structure called the accessibility tree.
This tree distills the UI into semantic roles, states, and properties meaningful to screen readers, magnifiers, and other AT. It merges HTML semantics, ARIA attributes, and some computed styles.
When you press Tab or listen with a screen reader, you’re hearing the accessibility tree’s story, not the raw DOM.
Shadow DOM: encapsulation’s double-edged sword
Shadow DOM lets you build self-contained components. It hides implementation details, styles, and markup inside a shadow root, preventing conflicts and accidental overrides.
But this encapsulation comes at a cost. The shadow root acts like a boundary that the browser’s accessibility tree builder must navigate carefully. Not every node inside a shadow root is necessarily exposed to assistive tech.
Why Shadow DOM can break accessibility trees
Here’s the kicker: when a browser constructs the accessibility tree, it treats shadow boundaries differently than light DOM. Some elements inside shadow roots may be omitted or flattened depending on browser heuristics and the component’s ARIA usage.
For example, if your shadow root contains a button element but you don’t expose it properly, the screen reader may see just a generic container or nothing at all.
Different browsers and screen readers handle this with subtle differences, making debugging extra tricky.
How I debugged the shadow accessibility mess
I started by inspecting the accessibility tree. Chrome DevTools has an Accessibility pane that shows the accessibility tree hierarchy. I compared what was inside my component’s shadow root to what was actually exposed.
I found:
- The shadow root was represented as a node but its internals were missing or collapsed.
- ARIA roles I set inside the shadow root weren’t picked up.
- Keyboard focus was trapped or skipped unexpectedly.
This told me the accessibility tree builder wasn’t bridging the shadow boundary properly.
Strategies to fix Shadow DOM accessibility
1. Use delegatesFocus when creating your shadow root
When you create a shadow root with element.attachShadow({ mode: 'open', delegatesFocus: true }), it allows the shadow DOM to delegate focus to internal elements. This helps keyboard users and screen readers interact with inner controls naturally.
2. Expose semantics explicitly with ARIA
Inside your shadow DOM, use ARIA roles, properties, and states carefully. For example, if your shadow root wraps a listbox, mark the container with role="listbox" and internal items with role="option".
But remember: ARIA can’t fix everything. It’s better to use native semantics where possible.
3. Use aria-hidden and tabindex strategically
Sometimes, elements inside the shadow root should be hidden from the accessibility tree (like decorative wrappers). Mark them with aria-hidden="true".
Also, ensure only interactive elements inside the shadow root have tabindex="0" or greater, so focus doesn’t get lost.
4. Flatten the accessibility tree with ::part and ::slotted
The ::part and ::slotted pseudo-elements let you expose parts of your shadow DOM to styling and accessibility.
By exposing interactive parts via part, you let assistive tech recognize them more easily. Slotted content from light DOM is incorporated in the accessibility tree too.
5. Test across browsers and screen readers
Accessibility tree handling varies. Test with Chrome + NVDA, Safari + VoiceOver, Firefox + Orca, etc. Tools like the Accessibility Insights browser extension help visualize the accessibility tree.
A concrete example: making a shadow dropdown accessible
Here’s a simplified pattern I used for a dropdown inside Shadow DOM:
const root = this.attachShadow({ mode: 'open', delegatesFocus: true });
root.innerHTML = `
<div role="listbox" tabindex="0">
<div role="option" tabindex="-1">Option 1</div>
<div role="option" tabindex="-1">Option 2</div>
</div>
`;
- The
listbox container is focusable and has the right role.
- Each option is marked with
role="option" and is not focusable by tab (tabindex="-1") but focus is managed programmatically.
This setup showed up correctly in the accessibility tree and the screen reader announced options as expected.
Final tip: Shadow DOM doesn’t have to be an accessibility black hole
Shadow DOM can feel like a barrier to accessibility, but with some care, you can build components that communicate clearly with assistive tech.
Use browser devtools to peek at the accessibility tree, leverage delegatesFocus, ARIA roles, and test with real screen readers early and often.
Once you decode how shadow boundaries shape the accessibility tree, you’ll avoid surprises and build components everyone can use.