Web Components Deep Dive: Shadow DOM, Custom Elements, and Native Browser Componentization

前端工程(Updated Jun 1, 2026)

Web Components: The Browser-Native Component Model

Web Components are a W3C standard for native browser componentization, built on three core APIs:

API Purpose Status
Custom Elements Define new HTML tags Stable, full browser support
Shadow DOM Encapsulate styles and DOM Stable, full browser support
HTML Templates Declare reusable DOM fragments Stable, full browser support

CSS Parts (::part()) is a companion API for piercing Shadow DOM styling.


Custom Elements: Define Your HTML Tags

Lifecycle Callbacks

class MyTooltip extends HTMLElement {
  static get observedAttributes() {
    return ['position', 'content'];
  }

  constructor() {
    super();
    console.log('1. constructor — initialization');
  }

  connectedCallback() {
    console.log('2. connectedCallback — inserted into DOM');
    this.render();
  }

  disconnectedCallback() {
    console.log('3. disconnectedCallback — removed from DOM');
    this.cleanup();
  }

  attributeChangedCallback(name: string, oldVal: string, newVal: string) {
    console.log(`4. attributeChanged: ${name} ${oldVal} → ${newVal}`);
    if (oldVal !== newVal) this.update(name, newVal);
  }

  adoptedCallback() {
    console.log('5. adoptedCallback — moved to new document');
  }
}

customElements.define('my-tooltip', MyTooltip);

Registration Rules

customElements.define('my-button', MyButton);   // ✅
customElements.define('button', MyButton);       // ❌ Not allowed

customElements.define('my-button', MyButton);    // ✅
customElements.define('my-button', MyButton2);   // ❌ Already registered

const MyBtn = customElements.get('my-button');
const defined = customElements.whenDefined('my-button');
defined.then(() => console.log('my-button is ready'));

Shadow DOM: Style and Structure Encapsulation

Open vs. Closed Mode

class ShadowComponent extends HTMLElement {
  constructor() {
    super();
    // Open: external code can access element.shadowRoot
    this.attachShadow({ mode: 'open' });
    // Closed: external code gets null from element.shadowRoot
    // this.attachShadow({ mode: 'closed' });
  }
}
Feature open closed
element.shadowRoot Returns Shadow Root Returns null
External query inside Yes No
querySelector piercing Yes No
Usage in practice High Very low

Style Encapsulation in Practice

class StyledCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          border-radius: 8px;
          overflow: hidden;
          background: #fff;
          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        :host([highlighted]) {
          border: 2px solid #4f46e5;
        }
        ::slotted(h2) {
          margin: 0;
          padding: 16px;
          color: #1f2937;
        }
        .content { padding: 0 16px 16px; }
      </style>
      <slot name="header"></slot>
      <div class="content"><slot></slot></div>
    `;
  }
}
customElements.define('styled-card', StyledCard);

Usage:

<styled-card highlighted>
  <h2 slot="header">Card Title</h2>
  <p>Card content with fully encapsulated styles</p>
</styled-card>

CSS Parts: Piercing Shadow DOM Customization

shadow.innerHTML = `
  <style>.title { font-size: 18px; }</style>
  <h2 class="title" part="title"><slot></slot></h2>
`;
styled-card::part(title) {
  font-size: 24px;
  color: #e11d48;
}

HTML Templates and Slots

Template: Declarative DOM Fragments

<template id="card-template">
  <style>
    .card { padding: 16px; border: 1px solid #e5e7eb; border-radius: 8px; }
  </style>
  <div class="card">
    <slot name="title"></slot>
    <slot></slot>
  </div>
</template>
class TemplateCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    const template = document.getElementById('card-template') as HTMLTemplateElement;
    shadow.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('template-card', TemplateCard);

Comparison with Framework Components

Dimension Web Components React Vue
Standard W3C standard Community ecosystem Community ecosystem
Style isolation Shadow DOM (native) CSS Modules / CSS-in-JS Scoped CSS
Cross-framework reuse Native support Needs wrapper Needs wrapper
Reactivity Manual (attributeChangedCallback) Auto (setState/hooks) Auto (ref/reactive)
Templates String / Template JSX SFC Template
Ecosystem/tooling Limited Very rich Rich
SSR Limited support Full support Full support
DX Low-level High-level abstraction High-level abstraction

Practice: Building a Star Rating Component

class StarRating extends HTMLElement {
  private _value = 0;
  private _max = 5;

  static get observedAttributes() {
    return ['value', 'max'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.setupEvents();
  }

  attributeChangedCallback(name: string, _: string, newVal: string) {
    if (name === 'value') this._value = Number(newVal);
    if (name === 'max') this._max = Number(newVal);
    if (this.shadowRoot) this.render();
  }

  private render() {
    const stars = Array.from({ length: this._max }, (_, i) => {
      const filled = i < this._value;
      return `<span class="star ${filled ? 'filled' : ''}" data-index="${i}">★</span>`;
    }).join('');

    this.shadowRoot!.innerHTML = `
      <style>
        :host { display: inline-flex; gap: 4px; cursor: pointer; }
        .star { font-size: 24px; color: #d1d5db; transition: color 0.15s; }
        .star.filled { color: #f59e0b; }
        .star:hover { color: #fbbf24; }
      </style>
      ${stars}
    `;
  }

  private setupEvents() {
    this.shadowRoot!.addEventListener('click', (e) => {
      const target = e.target as HTMLElement;
      if (target.classList.contains('star')) {
        const index = Number(target.dataset.index) + 1;
        this.setAttribute('value', String(index));
        this.dispatchEvent(new CustomEvent('rate', {
          detail: { value: index },
          bubbles: true,
          composed: true
        }));
      }
    });
  }
}
customElements.define('star-rating', StarRating);

Usage:

<star-rating value="3" max="5"></star-rating>
<script>
  document.querySelector('star-rating')
    .addEventListener('rate', (e) => {
      console.log('Rated:', e.detail.value);
    });
</script>

Browser Compatibility

API Chrome Firefox Safari Edge
Custom Elements v1 54+ 63+ 10.1+ 79+
Shadow DOM v1 53+ 63+ 10.1+ 79+
HTML Templates 35+ 22+ 9+ 79+
CSS ::part() 73+ 72+ 13.1+ 79+
adoptedCallback 73+ 63+ 15.4+ 79+

All major browsers have full support as of 2026—no polyfills needed.


Best Practices

  1. Naming convention: Use org-component prefix to avoid collisions (e.g. toolsku-color-picker)
  2. Prefer open mode: Closed mode is hard to debug with limited benefit
  3. Attribute reflection: Sync key properties to DOM attributes for CSS selectors
  4. Composed events: Set composed: true on events that must cross Shadow DOM
  5. Lazy registration: Use customElements.whenDefined to manage dependency order

Summary

Web Components provide framework-agnostic component primitives ideal for building cross-project reusable UI atoms. While DX falls short of React/Vue abstractions, their style encapsulation and native standard advantages are irreplaceable in micro-frontends and design systems.

Use Code Playground to quickly test Web Components code and SVG Editor to create inline component icons.

Try these browser-local tools — no sign-up required →

#Web Components#Shadow DOM#Custom Elements#组件化#浏览器原生