Skip to content

Polymer Shadow DOM — Encapsulation, Styling, and Slot-Based Composition

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Polymer Shadow Dom. We cover key concepts, practical examples, and best practices to help you master this topic.

Shadow DOM provides DOM and style Encapsulation for Web Components — it isolates the component's internal structure from the main document, preventing style leaks and DOM conflicts.

What You'll Learn

  • Open vs closed shadow modes
  • Style encapsulation with :host
  • Slots for content projection
  • Named slots and fallback content
  • Shadow DOM events and retargeting

Why It Matters

Without Shadow DOM, component styles conflict with global styles. Shadow DOM ensures components work consistently regardless of the surrounding page.

Real-World Use

A design system where each component's internal DOM and styles are completely isolated from the host page.

Shadow DOM Architecture

flowchart TD
    A[Shadow DOM] --> B[Shadow Root]
    B --> C[Shadow Tree]
    B --> D[Slot Elements]
    A --> E[Style Encapsulation]
    E --> F[:host Styles]
    E --> G[Scoped Styles]
    A --> H[Event Retargeting]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Open vs Closed Shadow DOM

class OpenElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = '<p>Open shadow DOM</p>';
  }
}
customElements.define('open-element', OpenElement);

class ClosedElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'closed' });
    this.shadowRoot.innerHTML = '<p>Closed shadow DOM</p>';
  }
}
customElements.define('closed-element', ClosedElement);
document.querySelector('open-element').shadowRoot; // Returns shadow root
document.querySelector('closed-element').shadowRoot; // null

Expected output: Open shadow DOM is accessible via element.shadowRoot. Closed returns null.

:host Selector

class HostDemo extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; padding: 16px; border: 2px solid #1a237e; border-radius: 8px; }
        :host(.primary) { background: #1a237e; color: white; }
        :host(:hover) { border-color: #ff6f00; }
        :host-context(.dark-theme) { background: #333; color: #eee; }
      </style>
      <p>Host element styled with :host</p>
    `;
  }
}
customElements.define('host-demo', HostDemo);

Expected output: The host-demo styles apply to the custom element itself. :host(.primary) applies when the element has the primary class.

Slot-Based Content Projection

class CardContainer extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <div class="card">
        <div class="header"><slot name="title">Default Title</slot></div>
        <div class="content"><slot></slot></div>
        <div class="footer"><slot name="footer"></slot></div>
      </div>
    `;
  }
}

customElements.define('card-container', CardContainer);
<card-container>
  <span slot="title">My Card</span>
  <p>Card body content.</p>
  <div slot="footer"><button>Action</button></div>
</card-container>

Expected output: The card renders with title, body, and footer via slots. Named slots match the slot attribute on light DOM children.

Fallback Content

class ButtonGroup extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: flex; gap: 8px; }
        ::slotted(button) { padding: 8px 16px; border: 1px solid #ccc; border-radius: 4px; }
        ::slotted(.primary) { background: #1a237e; color: white; }
      </style>
      <slot>
        <button class="primary">OK</button>
        <button>Cancel</button>
      </slot>
    `;
  }
}

customElements.define('button-group', ButtonGroup);

Expected output: When no children are provided, the slot displays fallback content (OK/Cancel buttons). When children are provided, they replace the fallback.

::slotted Selector

class StyledList extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        ::slotted(li) { padding: 8px 12px; border-bottom: 1px solid #eee; list-style: none; }
        ::slotted(li:last-child) { border-bottom: none; }
        ::slotted(.active) { background: #e8eaf6; font-weight: bold; }
      </style>
      <ul><slot></slot></ul>
    `;
  }
}

customElements.define('styled-list', StyledList);

Expected output: Slotted list items receive styling via ::slotted. Only top-level slotted children are affected — descendants are not.

Shadow DOM Events

class EventDemo extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<button id="btn">Click</button><div id="log"></div>`;
    this.shadowRoot.querySelector('#btn').addEventListener('click', () => {
      this.shadowRoot.querySelector('#log').textContent = 'Clicked internally';
    });
  }
}

customElements.define('event-demo', EventDemo);

// External listener
document.querySelector('event-demo').addEventListener('click', (e) => {
  // e.target is the host, not the internal button
  console.log('Composed path:', e.composedPath());
});

Expected output: Events inside shadow DOM fire normally. External listeners see retargeted target (the host element). composedPath() reveals full path.

Composed Events

class ComposedEventDemo extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<button id="btn">Click</button>`;
    this.shadowRoot.querySelector('#btn').addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('action', {
        detail: { message: 'Button clicked' },
        bubbles: true,
        composed: true
      }));
    });
  }
}

customElements.define('composed-event', ComposedEventDemo);

Expected output: Custom events need composed: true to cross the shadow boundary. Without it, external listeners do not receive the event.

Common Mistakes

  1. Using closed mode unnecessarily - Closed prevents testing tools. Use open mode unless needed.

  2. Styling slotted content with global rules - ::slotted only selects top-level children.

  3. Forgetting composed: true - Custom events inside shadow DOM need composed: true.

  4. Targeting slotted content from inside shadow - Use ::slotted() selector.

  5. Assuming styles pierce shadow boundary - Global CSS does not affect shadow DOM.

Practice Questions

  1. What is the difference between open and closed shadow mode?
  2. How do you style the host element of a custom element?
  3. What is the purpose of the slot element?
  4. How do you style slotted content from within the shadow DOM?
  5. How do you dispatch an event from shadow DOM that is heard externally?

Challenge: Build a <accordion-panel> with shadow DOM encapsulation, named slot for header, default slot for content, collapsible/expand behavior, composed events, and ::slotted styling.

FAQ

Can I use external CSS frameworks inside Shadow DOM?

Yes, but styles must be included within the shadow root — either inline or via adoptedStyleSheets.

Does Shadow DOM affect form submissions?

Form elements inside shadow DOM are not auto-associated with external forms. Use ElementInternals.

How do I share styles across Shadow DOM components?

Use CSS custom properties (inherited), constructable stylesheets, or adoptedStyleSheets.

Can I have nested shadow roots?

Yes. A custom element inside a shadow tree can have its own shadow root.

Does Shadow DOM affect accessibility?

No. Shadow DOM preserves accessibility. Slotted content maintains its semantic meaning.

Mini Project

Build a media player component with shadow DOM encapsulation, named slots for title and controls, ::slotted styling, composed custom events (play, pause), and themed styling via CSS custom properties.

What's Next

Shadow DOM encapsulates components. Learn how Polymer HTML Templates create reusable markup.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro