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 年現在、主要ブラウザはすべて完全対応。ポリフィル不要。
ベストプラクティス
- 命名規約:
org-componentプレフィックスで衝突回避(例:toolsku-color-picker) - open モード推奨:closed モードはデバッグ困難でメリットが限定的
- 属性反映:主要プロパティを DOM 属性に同期し、CSS セレクタで利用可能に
- composed イベント:Shadow DOM を越えるイベントには
composed: true - 遅延登録:
customElements.whenDefinedで依存順序を管理
まとめ
Web Components はフレームワーク非依存のコンポーネントプリミティブで、クロスプロジェクト再利用可能な UI 原子コンポーネントの構築に適しています。React/Vue の高レイヤ抽象には及ばない開発体験ですが、スタイルカプセル化とネイティブ標準の利点はマイクロフロントエンドやデザインシステムで代替不可能です。
コードプレイグラウンド で Web Components を素早く検証し、SVG エディタ でコンポーネント用アイコンを作成できます。
ブラウザローカルツールを無料で試す →
#Web Components#Shadow DOM#Custom Elements#组件化#浏览器原生