Nested Shadow Roots — Complete Guide
In this tutorial, you will learn about Nested Shadow Roots. We cover key concepts, practical examples, and best practices to help you master this topic.
Nested Shadow Roots allow custom elements to contain other custom elements, creating complex component hierarchies while maintaining Encapsulation at every level.
What You'll Learn
- How to nest custom elements with their own shadow roots
- How style encapsulation works across nested shadow boundaries
- How events propagate through nested shadow trees
- How to communicate between nested shadow components
Why It Matters
Real-world components are rarely flat. A date picker contains dropdowns. A data table contains rows. A dialog contains buttons. Each nested level benefits from its own encapsulation.
flowchart LR A[Page] --> B[Outer Component] B --> C[Inner Component 1] B --> D[Inner Component 2] C --> E[Deepest Component] B -.->|Shadow boundary 1| C C -.->|Shadow boundary 2| E
Basic Nesting
Each custom element has its own shadow root. Nesting them is simply a matter of using one custom element inside another's shadow tree.
class InnerComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
div { background: #e8f4f8; padding: 12px; border-radius: 4px; }
</style>
<div>
<h4>Inner Component</h4>
<p>This has its own shadow root</p>
</div>
`;
}
}
customElements.define('inner-component', InnerComponent);
class OuterComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.container { background: #f0f0f0; padding: 16px; border-radius: 8px; }
</style>
<div class="container">
<h3>Outer Component</h3>
<inner-component></inner-component>
</div>
`;
}
}
customElements.define('outer-component', OuterComponent);
// HTML: <outer-component></outer-component>
// Renders outer shadow with inner-component inside,
// which itself has its own shadow root
Style Isolation Across Nested Roots
Each shadow root's styles are completely isolated. An inner component's styles do not affect the outer component, and vice versa.
class BlueComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
h3 { color: blue; font-size: 24px; }
p { color: darkblue; }
</style>
<div>
<h3>Blue Component</h3>
<p>Text is dark blue</p>
</div>
`;
}
}
customElements.define('blue-component', BlueComponent);
class RedContainer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
h3 { color: red; font-size: 18px; }
</style>
<div>
<h3>Red Container</h3>
<p>Red container paragraph</p>
<blue-component></blue-component>
</div>
`;
}
}
customElements.define('red-container', RedContainer);
// The outer h3 is red, 18px.
// The inner h3 (inside blue-component) is blue, 24px.
// No style conflict because each has its own shadow root.
Event Propagation Through Nested Roots
Events bubble through nested shadow boundaries with retargeting at each level.
class DeepButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<button id="deepBtn">Deep</button>';
}
}
customElements.define('deep-button', DeepButton);
class MiddlePanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<deep-button></deep-button>';
this.shadowRoot.addEventListener('click', (e) => {
// At this level, event.target is <deep-button> (retargeted)
console.log('Middle panel sees target:', e.target.tagName);
});
}
}
customElements.define('middle-panel', MiddlePanel);
class OuterShell extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<middle-panel></middle-panel>';
this.shadowRoot.addEventListener('click', (e) => {
// At this level, event.target is <middle-panel> (retargeted again)
console.log('Outer shell sees target:', e.target.tagName);
});
}
}
customElements.define('outer-shell', OuterShell);
// Clicking the button logs:
// Middle panel sees target: DEEP-BUTTON
// Outer shell sees target: MIDDLE-PANEL
Communicating Between Nested Components
Use custom events with composed: true for child-to-parent communication across nested boundaries.
class NestedChild extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<button id="notify">Notify Up</button>';
this.shadowRoot.getElementById('notify').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('child-action', {
bubbles: true,
composed: true, // Must be true to cross all shadow boundaries
detail: { source: 'NestedChild', timestamp: Date.now() }
}));
});
}
}
customElements.define('nested-child', NestedChild);
class NestedParent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<nested-child></nested-child>';
// Listen on the host to catch events from deeper shadow roots
this.addEventListener('child-action', (e) => {
console.log('Parent received:', e.detail);
// Modify and re-dispatch if needed
});
}
}
customElements.define('nested-parent', NestedParent);
Passing Data Down Through Slots
Use slots to project Light DOM content through multiple levels of nesting.
class DeepSlot extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style> .box { border: 2px solid #3498db; padding: 8px; margin: 4px; } </style>
<div class="box">
<h4>Deep Slot</h4>
<slot></slot>
</div>
`;
}
}
customElements.define('deep-slot', DeepSlot);
class MidSlot extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style> .box { border: 2px solid #e74c3c; padding: 8px; margin: 4px; } </style>
<div class="box">
<h4>Mid Slot</h4>
<deep-slot><slot></slot></deep-slot>
</div>
`;
}
}
customElements.define('mid-slot', MidSlot);
// HTML:
// <mid-slot>
// <p>Content from Light DOM</p>
// </mid-slot>
// The <p> projects through mid-slot's <slot> into deep-slot's <slot>
Common Mistakes
- Assuming nested shadow roots share styles — each shadow root is completely isolated.
- Forgetting
composed: trueon custom events, causing them to be trapped in the innermost shadow tree. - Expecting event.target to show the deepest element — retargeting changes it at each boundary.
- Creating deeply nested shadow trees (more than 3-4 levels) without considering performance.
- Trying to access an inner component's shadow root from the outer component's code.
Practice Questions
- Are nested shadow roots style-isolated? Yes. Each shadow root has its own completely isolated styles.
- How do events propagate across nested shadow boundaries? With retargeting at each level, showing the host element at the boundary.
- How do you communicate from a deeply nested component to the top? Use composed custom events that bubble through all boundaries.
- Can you pass content through multiple levels of nested slots? Yes. Slot nested slots project Light DOM through multiple shadow boundaries.
Challenge
Build a three-level nested form component: Form (outer) contains Fieldset (middle) contains Input (inner). Each level has its own shadow root. The Input should notify the Form of value changes via composed custom events. The Form should collect all values on submit.
FAQ
Mini Project
Build a nested comment thread component. Each comment is a custom element with its own shadow root. Replies are nested comments inside the parent's shadow tree. Implement composed events for upvoting and replying. Measure rendering performance with 100 nested levels.
What's Next
Lesson 17: Shadow DOM and Accessibility
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro