<br />

Web features: [Shadow DOM](https://webstatus.dev/features/shadow-dom) [\<slot\>](https://webstatus.dev/features/slot)

> [!NOTE]
> **Note:** These guides are authored for AI coding agents (apologies for the robotic tone!). To have your coding agent adopt these patterns automatically, check out [Modern Web Guidance](https://github.com/GoogleChrome/modern-web-guidance).

A shadow root is an encapsulated DOM subtree attached to a host element: styles inside it do not leak out, and page styles do not leak in (except inherited properties). Templates construct the tree efficiently; slots project Light DOM content into it.

## Attaching a shadow root

    this.attachShadow({ mode: 'open', delegatesFocus: true });

`delegatesFocus: true` makes focusing the host move focus to the first focusable element inside the shadow tree, routes `:focus` styling to the host, and on form-associated custom elements (`static formAssociated = true`) forwards `<label>` clicks to the inner control. Set it whenever the component wraps focusable controls. Use `mode: 'open'` in almost all cases so dev tools, page scripts, and server hydration can reach `element.shadowRoot` (see [Web Components Orientation](https://developer.chrome.com/docs/modern-web-guidance/guides/html/web-components)).

## Templates: construct the tree efficiently

Define a `<template>` **once** (module scope or a static field) and clone it per instance. Cloning a parsed fragment is far faster than re-parsing an `innerHTML` string on every construction.

    const template = document.createElement('template');
    template.innerHTML = `
      <div class="card">
        <slot name="header"></slot>
        <div class="body"><slot></slot></div>
        <slot name="footer"></slot>
      </div>
    `;

    class MyCard extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        // MANDATORY: deep-clone the template content so each instance gets its own nodes.
        this.shadowRoot.append(template.content.cloneNode(true));
      }
    }
    customElements.define('my-card', MyCard);

Keep templates lean. Deeply nested "div soup" and many levels of nested shadow roots both add measurable style-resolution and layout cost, a major performance hit at scale. Prefer a flat structure and semantic elements.

## Slots: the parts that surprise people

Default vs. named slots work as documented; these are the behaviors that bite:

- **Whitespace counts.** Text nodes between elements (including pure whitespace) are assigned to the default slot, so a default slot is rarely truly "empty."
- **DOM order is preserved, not slot order.** Projected nodes keep their original Light DOM order for `:first-child`/`:last-child` purposes, regardless of which slot they fill. A `::slotted(*:first-child)` rule targets the first *DOM* child, not the first child of that slot, so it often will not match what you expect.
- **`slotchange` is the upgrade-timing signal.** It fires when assigned nodes change (including initial assignment) and is the reliable way to know Light DOM children have arrived after an upgrade, when they may not have existed during `connectedCallback`.

    this.shadowRoot.querySelector('slot[name="header"]')
      .addEventListener('slotchange', (e) => {
        const nodes = e.target.assignedElements();
        this.toggleAttribute('has-header', nodes.length > 0);
      });

## Loading stylesheets into a shadow root

| Method | Pros | Cons |
|---|---|---|
| `<style>` in the template | Simple, fully encapsulated, works without JS. | Duplicated per instance; bloats the DOM at scale. |
| `<link rel="stylesheet">` | Cacheable external file. | Extra request; risks a flash of unstyled content. |
| `adoptedStyleSheets` | One shared `CSSStyleSheet` across all instances; fastest at scale. | Requires JS; constructed sheets don't support `@import`. |

For many instances of one component, prefer a shared constructed stylesheet:

    const sheet = new CSSStyleSheet();
    sheet.replaceSync(`:host { display: block; }`);

    class MyCard extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        // The same sheet object is reused by every instance; no per-instance parse.
        this.shadowRoot.adoptedStyleSheets = [sheet];
      }
    }

Source: [shadow-dom guide on GitHub](https://github.com/GoogleChrome/modern-web-guidance-src/blob/main/guides/html/shadow-dom/guide.md)