Focus Management in Shadow DOM — Complete Guide
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
- Forgetting
delegatesFocus: truewhen the custom element needs to be reachable by Tab. - Assuming focus events inside the shadow tree propagate to the host without retargeting.
- Not handling
Tabkey events in modal dialogs, causing focus to escape the shadow tree. - Setting
tabindexon the host withoutdelegatesFocus, creating confusing focus behavior. - Calling
focus()on the host element when the intent is to focus an element inside the shadow tree.
Practice Questions
- What does
delegatesFocus: truedo? It makes the host focusable and delegates focus to the first focusable element in the shadow tree. - Are focus events composed? Yes, focus, blur, focusin, and focusout events are composed and cross shadow boundaries.
- How do you trap focus inside a Shadow DOM element? Listen for Tab keydown events and cycle through focusable elements manually.
- 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
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