Docs

README

25.2 Shadow DOM

Overview

Shadow DOM provides encapsulation for the DOM tree and styles. It allows you to attach a hidden DOM tree to an element, keeping your component's internal structure separate from the main document.

Learning Objectives

  • Understand the Shadow DOM concept
  • Create and attach shadow roots
  • Work with open and closed modes
  • Apply scoped styles
  • Use CSS custom properties for theming
  • Understand slot-based composition

What is Shadow DOM?

Shadow DOM creates an isolated DOM subtree:

<my-component>
  #shadow-root
    <style>...</style>
    <div class="internal">
      <slot></slot>
    </div>
</my-component>
  • Styles are scoped inside the shadow root
  • Internal structure is hidden from document queries
  • CSS and JavaScript from the document don't affect shadow content

Creating Shadow DOM

Attaching a Shadow Root

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

    // Attach shadow root
    const shadow = this.attachShadow({ mode: 'open' });

    // Add content
    shadow.innerHTML = `
            <style>
                p { color: blue; }
            </style>
            <p>This is in the shadow DOM</p>
        `;
  }
}

Open vs Closed Mode

// Open mode - shadow root accessible via element.shadowRoot
this.attachShadow({ mode: 'open' });
element.shadowRoot; // Returns the shadow root

// Closed mode - shadow root not accessible
const shadow = this.attachShadow({ mode: 'closed' });
element.shadowRoot; // Returns null

// Must store reference internally for closed mode
this._shadow = shadow;

Shadow DOM Styling

Scoped Styles

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

    this.shadowRoot.innerHTML = `
            <style>
                /* Only affects this component */
                p {
                    color: blue;
                    font-size: 16px;
                }
                
                .highlight {
                    background: yellow;
                }
            </style>
            <p class="highlight">Styled text</p>
        `;
  }
}

:host Selector

// Style the host element itself
this.shadowRoot.innerHTML = `
    <style>
        :host {
            display: block;
            padding: 16px;
            border: 1px solid #ccc;
        }
        
        /* Host with specific attribute */
        :host([theme="dark"]) {
            background: #333;
            color: white;
        }
        
        /* Host with class */
        :host(.active) {
            border-color: blue;
        }
        
        /* Host in specific context */
        :host-context(.sidebar) {
            max-width: 200px;
        }
    </style>
`;

::slotted Selector

// Style slotted content
this.shadowRoot.innerHTML = `
    <style>
        /* Style direct children in slot */
        ::slotted(*) {
            margin: 8px 0;
        }
        
        /* Style specific slotted elements */
        ::slotted(h2) {
            color: navy;
        }
        
        ::slotted(.highlight) {
            background: yellow;
        }
    </style>
    <slot></slot>
`;

CSS Custom Properties (Theming)

// Component with customizable styles
this.shadowRoot.innerHTML = `
    <style>
        :host {
            --primary-color: blue;
            --text-color: #333;
            --spacing: 16px;
        }
        
        .container {
            color: var(--text-color);
            padding: var(--spacing);
        }
        
        button {
            background: var(--primary-color);
            color: white;
        }
    </style>
    <div class="container">
        <slot></slot>
        <button>Action</button>
    </div>
`;
/* External CSS can override custom properties */
my-component {
  --primary-color: red;
  --text-color: navy;
}

Slots and Composition

Default Slot

// Component with default slot
this.shadowRoot.innerHTML = `
    <div class="card">
        <slot>Default content</slot>
    </div>
`;
<my-card>
  <p>This replaces the default</p>
</my-card>

Named Slots

// Component with named slots
this.shadowRoot.innerHTML = `
    <div class="card">
        <header>
            <slot name="header">Default Header</slot>
        </header>
        <main>
            <slot>Default Content</slot>
        </main>
        <footer>
            <slot name="footer"></slot>
        </footer>
    </div>
`;
<my-card>
  <h2 slot="header">Card Title</h2>
  <p>Main content goes here</p>
  <button slot="footer">Action</button>
</my-card>

Working with Slots Programmatically

class SlotHandler extends HTMLElement {
  connectedCallback() {
    const slot = this.shadowRoot.querySelector('slot');

    // Get assigned nodes
    const nodes = slot.assignedNodes();
    const elements = slot.assignedElements();

    // Listen for slot changes
    slot.addEventListener('slotchange', (e) => {
      console.log('Slot content changed');
      const assigned = e.target.assignedElements();
      console.log('New elements:', assigned);
    });
  }
}

Shadow DOM and Events

Event Retargeting

// Events are retargeted at shadow boundary
shadow.innerHTML = `<button>Click me</button>`;

shadow.querySelector('button').addEventListener('click', (e) => {
  console.log(e.target); // <button>
});

// Outside, event.target is the host
hostElement.addEventListener('click', (e) => {
  console.log(e.target); // <my-component>
  console.log(e.composedPath()); // Full path through shadow
});

Composed Events

// Create event that crosses shadow boundary
button.dispatchEvent(
  new CustomEvent('my-event', {
    bubbles: true,
    composed: true, // Crosses shadow boundary
    detail: { data: 'value' },
  })
);

Delegating Focus

this.attachShadow({
  mode: 'open',
  delegatesFocus: true, // Focus delegates to first focusable element
});

Best Practices

  1. Use open mode unless you need true encapsulation
  2. Expose CSS custom properties for theming
  3. Use slots for flexible composition
  4. Keep shadow DOM shallow for performance
  5. Consider accessibility with proper ARIA attributes

Summary

FeatureDescription
attachShadow()Create shadow root
mode: 'open'Shadow root accessible
mode: 'closed'Shadow root hidden
:hostStyle the host element
::slotted()Style slotted content
<slot>Content projection
--custom-propertyThemeable styles

Resources

README - JavaScript Tutorial | DeepML