Skip to content

Shadow Root

This lesson explains Shadow Root and how it creates an encapsulated DOM tree inside a custom element, separating internal markup and styles from the rest of the page.

What Is a Shadow Root?

A shadow root is the top node of a Shadow DOM tree attached to a host element. It contains the component's private internal markup and styles, separate from the page's light DOM.

You create a shadow root with attachShadow(). Once attached, the component can render encapsulated structure while still projecting external content through slots.

Concept Description
Shadow Root Root node of an encapsulated DOM subtree
Host Element The custom element that owns the shadow root
Create API element.attachShadow({ mode })
Access API element.shadowRoot for open mode
Main Benefit Style and DOM encapsulation
Works With Slots, templates, and custom events

Basic Shadow Root Example

class InfoCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          padding: 1rem;
          border: 1px solid #dbe5f1;
          border-radius: 0.75rem;
        }
      </style>
      <article class="card">
        <slot></slot>
      </article>
    `;
  }
}

customElements.define("info-card", InfoCard);
<info-card>
  <h2>Team Updates</h2>
  <p>Design review is scheduled for Monday.</p>
</info-card>

The shadow root holds the card structure and styles, while slotted light DOM content appears inside the component.

How Shadow Root Works

Part Description
Host The custom element in the light DOM, such as <info-card>
Shadow Root The encapsulated root attached to the host
Shadow Tree Internal nodes inside the shadow root
Light DOM Markup supplied by the component consumer
Slots Projection points from light DOM into shadow tree

Creating a Shadow Root with attachShadow()

class AppButton extends HTMLElement {
  constructor() {
    super();

    const shadow = this.attachShadow({
      mode: "open"
    });

    shadow.innerHTML = `
      <style>
        button {
          padding: 0.5rem 1rem;
          border: none;
          border-radius: 0.5rem;
          background: #2563eb;
          color: white;
        }
      </style>
      <button type="button">
        <slot></slot>
      </button>
    `;
  }
}

Call attachShadow() once, usually in the constructor. After that, use the returned shadow root or this.shadowRoot to render internal content.

Open vs Closed Shadow Root

Mode Access When to Use
open element.shadowRoot is available Most components, testing, tooling, theming
closed Shadow root is hidden from outside code Strong encapsulation, third-party widgets
// Open shadow root
const openHost = document.createElement("info-card");
console.log(openHost.shadowRoot); // ShadowRoot

// Closed shadow root
class SecureWidget extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "closed" });
  }
}

const closedHost = new SecureWidget();
console.log(closedHost.shadowRoot); // null

Querying Inside a Shadow Root

class SearchField extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <label for="query">Search</label>
      <input id="query" type="search" />
    `;
  }

  connectedCallback() {
    this.input =
      this.shadowRoot.querySelector("#query");

    this.input.addEventListener("input", (event) => {
      this.dispatchEvent(
        new CustomEvent("search-change", {
          detail: { value: event.target.value },
          bubbles: true,
          composed: true
        })
      );
    });
  }
}

Query internal elements through this.shadowRoot. External page CSS and DOM queries do not reach inside the shadow tree by default.

Shadow Root with Templates

<template id="panel-template">
  <style>
    .panel { padding: 1rem; background: #f8fafc; }
  </style>
  <section class="panel">
    <slot name="title"></slot>
    <slot></slot>
  </section>
</template>
class PanelBox extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    const template =
      document.getElementById("panel-template");

    this.shadowRoot.appendChild(
      template.content.cloneNode(true)
    );
  }
}

Templates are a common way to populate a shadow root with reusable structure and scoped CSS.

Declarative Shadow Root

<profile-card>
  <template shadowrootmode="open">
    <style>
      :host { display: block; }
    </style>
    <article><slot></slot></article>
  </template>
  <p>Alex Chen</p>
</profile-card>

Declarative Shadow DOM lets the browser create a shadow root from HTML markup. This is useful for server-side rendering and progressive enhancement before JavaScript runs.

Shadow Root Encapsulation Boundary

Feature Inside Shadow Root Outside Shadow Root
CSS selectors Scoped to shadow tree Do not pierce by default
DOM queries Use shadowRoot.querySelector() Cannot see internal nodes directly
Slotted content Projected visually into slots Still owned by light DOM
Custom events Need composed: true to escape Heard by host/page listeners when composed

Styling the Host Element

this.shadowRoot.innerHTML = `
  <style>
    :host {
      display: block;
      margin-bottom: 1rem;
    }

    :host([disabled]) {
      opacity: 0.6;
      pointer-events: none;
    }
  </style>
  <button type="button">
    <slot></slot>
  </button>
`;

The :host selector styles the custom element itself from inside the shadow root, which is useful for layout and state styling based on host attributes.

Light DOM vs Shadow Root

Feature Light DOM Shadow Root
Ownership Consumer / page markup Component internal markup
Styling Global page CSS applies Scoped shadow CSS applies
Visibility Visible in page DOM tree Hidden behind encapsulation boundary
Best For Flexible slotted content Private structure and styles

When to Use a Shadow Root

  • You need to prevent internal CSS from leaking out or being overridden.
  • You want private DOM structure inside a reusable component.
  • You are building design system widgets with consistent styling.
  • You need slots for flexible content with encapsulated layout.
  • You want to avoid ID and class name collisions across apps.
  • You are distributing components to multiple teams or frameworks.

Common Shadow Root Use Cases

  • Buttons, badges, alerts, and cards in a component library.
  • Form controls with internal labels and validation UI.
  • Modals, drawers, and popovers with private layout structure.
  • Tabs and accordions with encapsulated panel markup.
  • Embeddable third-party widgets with strong style isolation.
  • Framework-agnostic UI packages shared across products.

Shadow Root Best Practices

  • Create the shadow root once in the constructor with attachShadow().
  • Prefer mode: "open" unless you have a strong reason not to.
  • Use templates or render methods to keep shadow markup organized.
  • Expose theming through CSS custom properties and ::part().
  • Use slots for flexible consumer content instead of hardcoding everything.
  • Emit composed custom events when parent apps need to listen.
  • Document which parts of the component are public vs internal.

Common Shadow Root Mistakes

  • Calling attachShadow() more than once on the same element.
  • Assuming global CSS will style internal shadow DOM nodes directly.
  • Trying to query internal nodes from the page without open shadow access.
  • Forgetting composed: true on events that must leave the shadow tree.
  • Putting all content in shadow DOM when slots would be more flexible.
  • Using closed mode and making testing or theming unnecessarily difficult.
  • Treating shadow encapsulation as a substitute for accessibility work.

Key Takeaways

  • A shadow root is the encapsulated DOM tree inside a custom element.
  • Create it with attachShadow() and use open or closed mode intentionally.
  • Internal markup and CSS stay separate from the page's light DOM.
  • Slots still allow flexible external content projection.
  • Shadow roots are a core building block of production Web Components.

Pro Tip

Think of the shadow root as the component's private implementation layer. Keep stable public APIs on the host element with attributes, properties, events, and slots, and put internal structure inside the shadow root.