Skip to content

Nested Shadow Roots — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

  1. Assuming nested shadow roots share styles — each shadow root is completely isolated.
  2. Forgetting composed: true on custom events, causing them to be trapped in the innermost shadow tree.
  3. Expecting event.target to show the deepest element — retargeting changes it at each boundary.
  4. Creating deeply nested shadow trees (more than 3-4 levels) without considering performance.
  5. Trying to access an inner component's shadow root from the outer component's code.

Practice Questions

  1. Are nested shadow roots style-isolated? Yes. Each shadow root has its own completely isolated styles.
  2. How do events propagate across nested shadow boundaries? With retargeting at each level, showing the host element at the boundary.
  3. How do you communicate from a deeply nested component to the top? Use composed custom events that bubble through all boundaries.
  4. 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

Can you have shadow roots inside other shadow roots?

Yes. Custom elements with shadow roots can be used inside other custom elements' shadow trees, creating nested encapsulation.

Do nested shadow roots affect performance?

Each shadow root adds overhead. 3-4 levels is fine, but deeply nested trees should be measured.

How does CSS scoping work with nested shadow roots?

Each shadow root scopes its own styles. Styles from an outer shadow root do not affect inner shadow trees.

Can an outer component access an inner component's shadow root?

Only if the inner shadow root mode is 'open' and the outer code queries it via DOM traversal, which breaks encapsulation.

Do events cross all nested shadow boundaries?

Only composed events cross all boundaries. Non-composed events stop at the first shadow boundary.

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