Web Components 深掘り:Shadow DOM・Custom Elements とブラウザネイティブコンポーネント化

前端工程(更新: 2026年6月1日)

Web Components:ブラウザネイティブのコンポーネントモデル

Web Components は W3C 標準 のブラウザネイティブコンポーネント化仕様で、3 つのコア API で構成されます:

API 役割 状態
Custom Elements 新しい HTML タグの定義 安定、全ブラウザ対応
Shadow DOM スタイルと DOM のカプセル化 安定、全ブラウザ対応
HTML Templates 再利用可能な DOM 断片の宣言 安定、全ブラウザ対応

補完 API として CSS Parts::part())があり、Shadow DOM のスタイルを外部からカスタマイズできます。


Custom Elements:独自 HTML タグの定義

ライフサイクルコールバック

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

  constructor() {
    super();
    console.log('1. constructor — 初期化');
  }

  connectedCallback() {
    console.log('2. connectedCallback — DOM に挿入');
    this.render();
  }

  disconnectedCallback() {
    console.log('3. disconnectedCallback — 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 — 新しい document へ移動');
  }
}

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

登録ルール

customElements.define('my-button', MyButton);   // ✅
customElements.define('button', MyButton);       // ❌ 許可されない

customElements.define('my-button', MyButton);    // ✅
customElements.define('my-button', MyButton2);   // ❌ 既に登録済み

const MyBtn = customElements.get('my-button');
const defined = customElements.whenDefined('my-button');
defined.then(() => console.log('my-button が利用可能'));

Shadow DOM:スタイルと構造のカプセル化

Open と Closed モード

class ShadowComponent extends HTMLElement {
  constructor() {
    super();
    // Open:外部から element.shadowRoot でアクセス可能
    this.attachShadow({ mode: 'open' });
    // Closed:外部から shadowRoot は null
    // this.attachShadow({ mode: 'closed' });
  }
}
特性 open closed
element.shadowRoot Shadow Root を返す null を返す
外部からの内部クエリ 可能 不可能
querySelector の貫通 可能 不可能
実際の使用頻度 高い 極めて低い

スタイルカプセル化の実践

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);

使用例:

<styled-card highlighted>
  <h2 slot="header">カードタイトル</h2>
  <p>カード内容、スタイルは完全にカプセル化</p>
</styled-card>

CSS Parts:Shadow DOM を貫通するカスタマイズ

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 と Slot

Template:宣言的 DOM テンプレート

<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);

フレームワークコンポーネントとの比較

観点 Web Components React Vue
標準 W3C 標準 コミュニティ コミュニティ
スタイル分離 Shadow DOM(ネイティブ) CSS Modules / CSS-in-JS Scoped CSS
複数フレームワーク間再利用 ネイティブ対応 ラッパーが必要 ラッパーが必要
リアクティビティ 手動(attributeChangedCallback) 自動(setState/hooks) 自動(ref/reactive)
テンプレート 文字列 / Template JSX SFC Template
エコシステム/ツール 限定的 非常に豊富 豊富
SSR 限定的 完全対応 完全対応
開発体験 低レイヤ 高レイヤ抽象 高レイヤ抽象

実践:星評価コンポーネントの構築

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);

使用例:

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

ブラウザ対応状況

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+

2026 年現在、主要ブラウザはすべて完全対応。ポリフィル不要。


ベストプラクティス

  1. 命名規約org-component プレフィックスで衝突回避(例:toolsku-color-picker
  2. open モード推奨:closed モードはデバッグ困難でメリットが限定的
  3. 属性反映:主要プロパティを DOM 属性に同期し、CSS セレクタで利用可能に
  4. composed イベント:Shadow DOM を越えるイベントには composed: true
  5. 遅延登録customElements.whenDefined で依存順序を管理

まとめ

Web Components はフレームワーク非依存のコンポーネントプリミティブで、クロスプロジェクト再利用可能な UI 原子コンポーネントの構築に適しています。React/Vue の高レイヤ抽象には及ばない開発体験ですが、スタイルカプセル化ネイティブ標準の利点はマイクロフロントエンドやデザインシステムで代替不可能です。

コードプレイグラウンド で Web Components を素早く検証し、SVG エディタ でコンポーネント用アイコンを作成できます。

ブラウザローカルツールを無料で試す →

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