Skip to content

Focus Management in Shadow DOM — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

Focus management in Shadow DOM involves handling focus events across shadow boundaries, delegating focus to shadow roots, and managing tab order for custom elements.

What You'll Learn

  • How focus events behave across shadow boundaries
  • What focus delegation is and how to use it
  • How to manage tab order with custom elements
  • How to implement accessible focus trapping

Why It Matters

Custom elements with Shadow DOM need proper focus management for keyboard Accessibility. Users who navigate with Tab or screen readers rely on correct focus behavior.

flowchart LR
  A[Tab key pressed] --> B{Element has Shadow DOM?}
  B -->|Yes with delegatesFocus| C[Focus enters shadow root]
  B -->|Yes without delegation| D[Focus skips to next focusable]
  B -->|No| E[Normal focus behavior]
  C --> F[Focus lands on first focusable inside]
  D --> G[Tab may skip the element entirely]

Focus Delegation

Shadow roots accept a delegatesFocus option. When true, the host element participates in tab order and delegates focus to the first focusable element inside the shadow tree.

class FocusInput extends HTMLElement {
    constructor() {
        super();
        // delegatesFocus: true makes the host focusable
        // and sends focus to the first focusable child
        this.attachShadow({ mode: 'open', delegatesFocus: true });
        this.shadowRoot.innerHTML = `
            <style>
                :host { display: inline-block; padding: 4px; border: 1px solid #ccc; }
                input { border: none; outline: none; padding: 4px; }
                input:focus { background: #f0f8ff; }
            </style>
            <input type="text" placeholder="Type here" id="innerInput">
        `;
    }
}
customElements.define('focus-input', FocusInput);

// When user tabs to <focus-input>, focus goes to the inner <input>
// The host shows :focus-within styling automatically

Tab Order Without Delegation

Without delegatesFocus, the host element may be skipped in tab order entirely. Focusable elements inside the shadow tree are not exposed to the page's tab sequence.

class NoDelegateFocus extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });  // No delegatesFocus
        this.shadowRoot.innerHTML = `
            <button id="btn1">Button 1</button>
            <button id="btn2">Button 2</button>
        `;
    }
}
customElements.define('no-delegate', NoDelegateFocus);

// Tab key skips <no-delegate> entirely.
// Buttons inside are not reachable via Tab from outside.
// User must click inside first to interact with shadow DOM buttons.

Handling Focus Events

Focus events (focus, blur, focusin, focusout) are composed and get retargeted across shadow boundaries.

class FocusTracker extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open', delegatesFocus: true });
        this.shadowRoot.innerHTML = `
            <input type="text" placeholder="Name" id="name">
            <input type="text" placeholder="Email" id="email">
        `;

        // Listen for focus events on the host
        this.addEventListener('focus', (e) => {
            // e.target is retargeted to the host
            console.log('Host received focus');
        });

        // Listen on the shadow root for internal focus changes
        this.shadowRoot.addEventListener('focusin', (e) => {
            // e.target is the actual focused element inside shadow
            console.log('Focused element:', e.target.id);
        });
    }
}
customElements.define('focus-tracker', FocusTracker);

Programmatic Focus Management

You can call focus() on elements inside the shadow tree. For custom elements to participate in tab order, set tabindex on the host.

class ProgrammaticFocus extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <input type="text" id="first" placeholder="First">
            <input type="text" id="second" placeholder="Second">
            <button id="submit">Submit</button>
        `;
    }

    // Expose a method to focus the first input
    focusFirst() {
        this.shadowRoot.getElementById('first').focus();
    }

    // Make the element focusable programmatically
    focus(options) {
        this.shadowRoot.getElementById('first').focus(options);
    }
}
customElements.define('programmatic-focus', ProgrammaticFocus);

// Usage
const el = document.querySelector('programmatic-focus');
el.focusFirst();  // Focuses the first input inside shadow DOM

Focus Trapping in Shadow DOM

Modal dialogs built with Shadow DOM need focus trapping to keep focus inside the dialog while open.

class FocusTrap extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                :host { display: none; }
                :host([open]) { display: block; position: fixed; inset: 0; background: rgba(0,0,0,0.5); }
                .dialog { background: white; margin: 20vh auto; padding: 24px; width: 400px; border-radius: 8px; }
            </style>
            <div class="dialog">
                <h2 id="title">Dialog</h2>
                <input type="text" placeholder="Name" id="name">
                <input type="text" placeholder="Email" id="email">
                <button id="closeBtn">Close</button>
            </div>
        `;
        this._focusableElements = null;
        this._currentFocusIndex = -1;
    }

    connectedCallback() {
        this.shadowRoot.getElementById('closeBtn').addEventListener('click', () => {
            this.removeAttribute('open');
        });

        this.shadowRoot.addEventListener('keydown', (e) => {
            if (e.key !== 'Tab') return;
            e.preventDefault();
            this._cycleFocus(e.shiftKey ? -1 : 1);
        });
    }

    _cycleFocus(direction) {
        if (!this._focusableElements) {
            this._focusableElements = Array.from(
                this.shadowRoot.querySelectorAll('input, button, [tabindex]:not([tabindex="-1"])')
            );
        }
        this._currentFocusIndex = (this._currentFocusIndex + direction + this._focusableElements.length) % this._focusableElements.length;
        this._focusableElements[this._currentFocusIndex].focus();
    }
}
customElements.define('focus-trap', FocusTrap);

Common Mistakes

  1. Forgetting delegatesFocus: true when the custom element needs to be reachable by Tab.
  2. Assuming focus events inside the shadow tree propagate to the host without retargeting.
  3. Not handling Tab key events in modal dialogs, causing focus to escape the shadow tree.
  4. Setting tabindex on the host without delegatesFocus, creating confusing focus behavior.
  5. Calling focus() on the host element when the intent is to focus an element inside the shadow tree.

Practice Questions

  1. What does delegatesFocus: true do? It makes the host focusable and delegates focus to the first focusable element in the shadow tree.
  2. Are focus events composed? Yes, focus, blur, focusin, and focusout events are composed and cross shadow boundaries.
  3. How do you trap focus inside a Shadow DOM element? Listen for Tab keydown events and cycle through focusable elements manually.
  4. What happens to tab order without delegatesFocus? The host element is skipped, and elements inside the shadow tree are not in the page tab order.

Challenge

Create a custom dialog component with delegatesFocus, focus trapping, and a close button. The dialog should trap Tab cycling inside itself, focus the first input when opened, and restore focus to the trigger element when closed.

FAQ

What is delegatesFocus in Shadow DOM?

delegatesFocus is an option in attachShadow that makes the shadow host focusable and automatically sends focus to the first focusable element inside the shadow tree.

Can I use delegatatesFocus with mode: closed?

Yes. delegatesFocus works with both open and closed shadow roots.

How do focus events behave across shadow boundaries?

Focus events are composed. They cross shadow boundaries and get retargeted so event.target shows the shadow host from outside listeners.

Does :focus-within work across shadow boundaries?

Yes. :focus-within on the host applies if any element inside the shadow tree is focused.

How do I make a specific element inside Shadow DOM receive focus?

Use element.shadowRoot.getElementById('id').focus() from outside, or expose a method on the custom element that focuses the desired internal element.

Mini Project

Build a custom search input component with delegatesFocus. The component should have a text input, a search icon, and a clear button. Implement keyboard handling so that pressing Escape clears the input and pressing Enter dispatches a search event.

What's Next

Lesson 13: Shadow DOM and Forms

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro