Skip to content

Shadow DOM and Accessibility — Complete Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Shadow Dom and Accessibility. We cover key concepts, practical examples, and best practices to help you master this topic.

Shadow DOM accessibility involves managing ARIA attributes across boundaries, projecting roles, and ensuring screen readers can navigate custom elements with scoped trees.

What You'll Learn

  • How ARIA attributes work across shadow boundaries
  • How to project ARIA roles and properties through slots
  • How to make custom elements with Shadow DOM accessible
  • How to test Shadow DOM components with assistive technology

Why It Matters

Shadow DOM can hide content from assistive technology if not handled correctly. Proper accessibility ensures all users can interact with custom elements regardless of how they consume web content.

flowchart LR
  A[Screen Reader] --> B[Accessibility Tree]
  B --> C[Shadow Root 1]
  B --> D[Shadow Root 2]
  C --> E[ARIA attributes projected]
  D --> F[ARIA attributes projected]
  E --> G[Host element with role]
  F --> G

ARIA Across Shadow Boundaries

ARIA attributes set on elements inside a shadow tree are not exposed to the outside. The browser computes the accessible name from the shadow tree and projects it to the host.

class AriaComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                .btn { padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
            </style>
            <button class="btn" id="actionBtn" aria-label="Submit form">
                Continue
            </button>
        `;
    }
}
customElements.define('aria-component', AriaComponent);

// The aria-label="Submit form" inside the shadow tree
// is not exposed to the accessibility tree directly.
// Instead, the browser computes the button's accessible name
// from its content ("Continue") and the shadow host
// contributes its own ARIA attributes to the accessibility tree.

Using role Attribute on the Host

Set ARIA roles on the host element itself, which is visible to the accessibility tree.

class AccessibleDialog extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; }
                .dialog { background: white; padding: 24px; border-radius: 8px; max-width: 500px; }
            </style>
            <div class="overlay">
                <div class="dialog" role="dialog" aria-labelledby="title" aria-describedby="desc">
                    <h2 id="title">Confirm Deletion</h2>
                    <p id="desc">Are you sure you want to delete this item? This action cannot be undone.</p>
                    <button id="confirm">Delete</button>
                    <button id="cancel">Cancel</button>
                </div>
            </div>
        `;
    }
}
customElements.define('accessible-dialog', AccessibleDialog);

// Set role="dialog" on the host to expose it to assistive technology
const dialog = document.querySelector('accessible-dialog');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-labelledby', 'title');
dialog.setAttribute('aria-modal', 'true');

Projecting ARIA Through Slots

When Light DOM content is projected into slots, its ARIA attributes remain intact and visible to the accessibility tree.

class AccessibleList extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                ul { list-style: none; padding: 0; }
                li { padding: 8px; border-bottom: 1px solid #eee; }
            </style>
            <ul role="list" aria-label="Interactive list">
                <slot></slot>
            </ul>
        `;
    }
}
customElements.define('accessible-list', AccessibleList);

// Usage:
// <accessible-list aria-label="Tasks">
//   <li role="listitem" aria-checked="false" tabindex="0">Buy groceries</li>
//   <li role="listitem" aria-checked="true" tabindex="0">Pay bills</li>
// </accessible-list>
//
// The <li> elements are projected into the slot.
// Their ARIA roles and states are preserved in the accessibility tree.

Accessibility with delegatesFocus

delegatesFocus improves keyboard accessibility by making the host element focusable.

class AccessibleInput extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open', delegatesFocus: true });
        this.shadowRoot.innerHTML = `
            <style>
                :host { display: inline-flex; align-items: center; gap: 4px; }
                label { font-weight: 500; }
                input { padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
                input:focus { outline: 2px solid #3498db; outline-offset: 2px; }
            </style>
            <label for="input">Name</label>
            <input type="text" id="input" aria-required="true">
        `;
    }
}
customElements.define('accessible-input', AccessibleInput);

// The label and input relationship (for="input") works across shadow boundary
// because the label and input are in the same shadow root.
// delegatesFocus makes Tab reach the component correctly.

Focus Management for Accessibility

Ensure visible focus indicators are present inside shadow roots. Custom elements must show focus styling.

class AccessibleButton extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                :host { display: inline-block; }
                .btn {
                    padding: 10px 20px;
                    background: #3498db;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    transition: outline-offset 0.1s;
                }
                .btn:focus-visible {
                    outline: 3px solid #2c3e50;
                    outline-offset: 2px;
                }
                .btn:hover { background: #2980b9; }
            </style>
            <button class="btn" id="btn" aria-label="Custom action">
                <slot></slot>
            </button>
        `;

        this.shadowRoot.getElementById('btn').addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                this.dispatchEvent(new CustomEvent('action-triggered', {
                    bubbles: true,
                    composed: true
                }));
            }
        });
    }
}
customElements.define('accessible-button', AccessibleButton);

Common Mistakes

  1. Assuming ARIA attributes inside shadow trees are automatically exposed to the accessibility tree.
  2. Not setting a role on the host element for custom interactive components.
  3. Forgetting visible focus indicators inside shadow roots for keyboard users.
  4. Using aria-labelledby to reference an element outside the shadow tree, which breaks the reference.
  5. Not testing with actual screen readers like NVDA, JAWS, or VoiceOver.

Practice Questions

  1. Are ARIA attributes inside shadow trees exposed to the accessibility tree? They are processed by the browser and contribute to the accessible name, but they are not directly exposed.
  2. How do you expose a role for a custom element using Shadow DOM? Set the role attribute on the host element itself.
  3. Does aria-labelledby work across shadow boundaries? No. aria-labelledby cannot reference elements in a different shadow tree.
  4. What is the best practice for focus indicators in Shadow DOM? Use :focus-visible in the shadow tree's styles with a visible outline.

Challenge

Build an accessible tab panel component with Shadow DOM. Each tab and panel is inside the shadow tree. Use proper ARIA roles (tablist, tab, tabpanel), aria-selected, aria-controls, and keyboard navigation. Test with a screen reader.

FAQ

Does Shadow DOM hide content from screen readers?

Not inherently. Screen readers can access shadow tree content. However, ARIA attributes must be correctly placed for proper semantic exposure.

How do I set ARIA attributes on a custom element with Shadow DOM?

Set ARIA attributes on the host element itself. Attributes inside the shadow tree are processed but not directly exposed.

Can I use aria-labelledby to reference elements inside Shadow DOM?

aria-labelledby only works for elements in the same shadow tree. You cannot cross shadow boundaries with ARIA references.

Does delegatesFocus affect accessibility?

Yes. delegatesFocus improves keyboard accessibility by making the host element reachable via Tab.

How do I test Shadow DOM accessibility?

Use real screen readers (NVDA, VoiceOver), the browser Accessibility panel, and axe-core automated testing.

Mini Project

Build an accessible custom checkbox component using Shadow DOM. The component should: use role="checkbox" on the host, show a visible check indicator, handle keyboard toggling with Space, show focus-visible styling, and announce state changes to screen readers via aria-live region.

What's Next

Lesson 18: ::part and ::slotted Deep Dive

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro