Skip to content

Shadow DOM

This lesson explains Shadow DOM and how it provides encapsulation for Web Components by separating internal markup and styles from the rest of the page while still supporting slots and custom events.

What Is Shadow DOM?

Shadow DOM is a browser feature that lets a custom element attach a hidden, encapsulated DOM tree. Internal markup and CSS stay scoped to the component, which helps prevent style conflicts and keeps implementation details private.

Shadow DOM is one of the core Web Components standards alongside Custom Elements and HTML Templates. It is widely used in design systems, component libraries, and reusable widgets.

Concept Description
Shadow DOM Encapsulated DOM subtree attached to a host element
Shadow Root The root node of the shadow tree
Host Element The custom element that owns the shadow tree
Create API element.attachShadow({ mode })
Main Benefit DOM and CSS encapsulation
Works With Slots, templates, and custom events

Basic Shadow DOM Example

class AlertBanner extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        .alert {
          padding: 1rem;
          border-radius: 0.5rem;
          background: #dbeafe;
          color: #1e3a8a;
        }
      </style>
      <div class="alert" role="alert">
        <slot></slot>
      </div>
    `;
  }
}

customElements.define("alert-banner", AlertBanner);
<alert-banner>
  Your profile was saved successfully.
</alert-banner>

The alert structure and styles live inside Shadow DOM, while the message content comes from the light DOM through the default slot.

How Shadow DOM Works

Layer Description
Light DOM Markup written by the page or component consumer
Host Element The custom element tag in the document
Shadow Root Entry point to the encapsulated internal tree
Shadow Tree Private internal elements and styles
Slots Bridges that project light DOM into shadow markup

Encapsulation Benefits

<style>
  .alert {
    background: red;
    color: white;
  }
</style>

<alert-banner>
  This keeps its own styles inside Shadow DOM.
</alert-banner>

Global page CSS does not automatically restyle internal Shadow DOM nodes. That makes components safer to reuse across apps with very different stylesheets.

  • Internal class names do not collide with page CSS.
  • Component styles do not leak out and restyle the page.
  • Private DOM structure is hidden from normal document queries.
  • Teams can refactor internals without breaking outside selectors.

Open vs Closed Shadow DOM

Mode Access When to Use
open element.shadowRoot is available Most reusable components and design systems
closed Shadow root hidden from outside JavaScript Strong encapsulation for third-party widgets
this.attachShadow({ mode: "open" });
// this.shadowRoot is available

this.attachShadow({ mode: "closed" });
// this.shadowRoot is null from outside

Shadow DOM with Slots

class AppCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #dbe5f1;
          border-radius: 0.75rem;
          padding: 1rem;
        }
      </style>
      <article class="card">
        <header><slot name="title"></slot></header>
        <div><slot></slot></div>
        <footer><slot name="footer"></slot></footer>
      </article>
    `;
  }
}
<app-card>
  <h2 slot="title">Billing</h2>
  <p>Your plan renews tomorrow.</p>
  <button slot="footer" type="button">Manage Plan</button>
</app-card>

Slots let Shadow DOM stay encapsulated while still accepting flexible content from the component consumer.

Styling Inside Shadow DOM

this.shadowRoot.innerHTML = `
  <style>
    :host {
      display: block;
    }

    :host([variant="danger"]) {
      --banner-bg: #fee2e2;
    }

    .banner {
      background: var(--banner-bg, #dbeafe);
    }

    ::slotted(h2) {
      margin-top: 0;
    }
  </style>
  <div class="banner">
    <slot></slot>
  </div>
`;
Selector Purpose
:host Styles the custom element host
:host(...) Styles host based on attribute or state
::slotted() Styles projected slotted content
CSS custom properties Allows theming from outside the shadow tree

Events and Shadow DOM Boundaries

this.shadowRoot.querySelector("button")
  .addEventListener("click", () => {
    this.dispatchEvent(
      new CustomEvent("action-click", {
        detail: { action: "save" },
        bubbles: true,
        composed: true
      })
    );
  });

Events that originate inside Shadow DOM do not automatically cross the encapsulation boundary. Set composed: true when parent applications need to listen for them.

Shadow DOM vs Light DOM Only

Feature Shadow DOM Light DOM Only
Style encapsulation Strong by default Depends on naming discipline
DOM privacy Internal tree is hidden All nodes are visible
Global CSS effect Limited on internal nodes Can affect all inner markup
Flexibility Great with slots Simple for basic components
Best For Design systems and reusable widgets Simple components with minimal styling risk

Declarative Shadow DOM

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

Declarative Shadow DOM allows a shadow tree to exist in HTML before JavaScript runs. This helps with server-side rendering and faster first render for encapsulated components.

When to Use Shadow DOM

  • You need reusable components with isolated CSS.
  • You want to prevent global styles from breaking widget internals.
  • You are building a design system for multiple apps.
  • You need private internal structure with public slots and APIs.
  • You distribute components across teams or frameworks.
  • You want to reduce class name and DOM selector collisions.

When Light DOM May Be Enough

  • The component is very simple and styling conflicts are unlikely.
  • You need maximum styling control from global app CSS.
  • You are prototyping quickly and do not need encapsulation yet.
  • Server rendering support is limited and declarative shadow is unavailable.
  • The component is used only inside one tightly controlled app.

Common Shadow DOM Use Cases

  • Buttons, badges, alerts, and cards in component libraries.
  • Form controls with internal labels and validation UI.
  • Modals, drawers, tabs, and accordions.
  • Embeddable widgets on third-party websites.
  • Micro frontend UI packages with isolated styling.
  • Framework-agnostic design system primitives.

Shadow DOM Best Practices

  • Attach the shadow root once in the constructor.
  • Prefer open mode unless closed encapsulation is truly needed.
  • Use slots for flexible consumer content.
  • Expose theming through CSS custom properties and parts.
  • Emit composed custom events when parent apps must listen.
  • Keep semantic HTML and accessibility inside the shadow tree.
  • Document public styling and interaction APIs clearly.

Common Shadow DOM Mistakes

  • Assuming Shadow DOM alone makes components accessible.
  • Using Shadow DOM for every component even when unnecessary.
  • Forgetting composed: true on important custom events.
  • Trying to style internal nodes with regular page CSS selectors.
  • Calling attachShadow() more than once on the same host.
  • Overusing closed shadow roots and blocking testing or theming.
  • Not providing slots or styling hooks for customization.

Key Takeaways

  • Shadow DOM encapsulates internal markup and CSS inside custom elements.
  • Create it with attachShadow() and choose open or closed mode intentionally.
  • Slots and custom events let encapsulated components stay flexible.
  • Shadow DOM is ideal for reusable widgets and design systems.
  • Good Shadow DOM design balances encapsulation with theming and accessibility.

Pro Tip

Use Shadow DOM when style isolation and private internals matter, but still expose a clean public API through attributes, slots, events, CSS custom properties, and documented styling hooks.