Web Components
ウェブコンポーネントは、一連のさまざまな技術です。これにより、再利用可能なカスタム要素を作成し、その機能を他のコードから分離してウェブアプリケーションで利用できるようにします。
3 つの主要技術
Web Components は 3 つの主要技術で構成されています。これらを組み合わせることで、カプセル化された機能を持つ多目的なカスタム要素を作成し、コードの衝突を恐れることなくどこでも再利用できます。
Custom Elements
独自の HTML 要素を JavaScript で定義し、ブラウザに登録する仕組み
カスタム要素とその動作を定義するための JavaScript API です。定義したカスタム要素は、通常の HTML 要素と同じようにページ内で使用できます。要素の名前にはハイフン(-)を含む必要があります(例: my-element)。
my-element user-card app-header myelement myElement header カスタム要素の名前にはハイフン(-)が必須です。これにより、ブラウザの標準要素との名前衝突を防ぎます。
class MyElement extends HTMLElement {
connectedCallback() {
this.innerHTML = '<p>Hello, Web Components!</p>';
}
}
customElements.define('my-element', MyElement); <!-- HTML で使う -->
<my-element></my-element> <my-element> HTMLElement を継承。完全に新しい要素を定義。
<p is="my-p"> 既存の HTML 要素を拡張。アクセシビリティを自動的に継承。
カスタム要素はブラウザによって管理される 4 つのライフサイクルコールバックを持っています。これらのコールバックを使って、要素の生成・DOM への接続・属性の変更・DOM からの削除に応じた処理を定義できます。
constructor() 要素を生成 connectedCallback() DOM に追加 attributeChangedCallback() 属性が変更 disconnectedCallback() DOM から削除 constructor() 要素が生成された時 要素の初期状態をセットアップする。super() の呼び出しが必須。Shadow DOM の作成はここで行う。
super()を呼ぶattachShadow()- 状態の初期化
- 属性の読み取り
- 子要素へのアクセス
innerHTMLの設定
connectedCallback() DOM に追加された時 要素が文書の DOM ツリーに接続された時に呼ばれる。レンダリング、イベントリスナーの登録、データ取得はここで行う。
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>:host { display: block; }</style>
<h2>${this.getAttribute('title')}</h2>
<slot></slot>
`;
this.addEventListener('click', this._onClick);
} attributeChangedCallback(name, oldVal, newVal) 監視対象の属性が変更された時 static observedAttributes で指定した属性が追加・削除・変更された時に呼ばれる。UI の更新をリアクティブに行える。
static observedAttributes = ['name', 'theme'];
attributeChangedCallback(name, oldVal, newVal) {
switch (name) {
case 'name':
this.shadowRoot.querySelector('.name').textContent = newVal;
break;
case 'theme':
this.shadowRoot.host.classList.toggle('dark', newVal === 'dark');
break;
}
} disconnectedCallback() DOM から削除された時 要素が DOM から切断された時に呼ばれる。イベントリスナーの解除、タイマーのクリア、外部リソースの解放を行う。メモリリーク防止に重要。
disconnectedCallback() {
this.removeEventListener('click', this._onClick);
clearInterval(this._timer);
this._observer?.disconnect();
} class UserCard extends HTMLElement {
static observedAttributes = ['name', 'avatar'];
attributeChangedCallback(attr, oldVal, newVal) {
if (attr === 'name') {
this.shadowRoot.querySelector('.name').textContent = newVal;
}
}
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: block; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
.name { font-weight: bold; font-size: 1.2rem; }
</style>
<div class="name">${this.getAttribute('name') ?? ''}</div>
<slot></slot>
`;
}
}
customElements.define('user-card', UserCard); <user-card name="Alice">Frontend Developer</user-card> Shadow DOM
コンポーネントの内部構造とスタイルを外部から隔離する仕組み
カプセル化された「シャドウ」DOM ツリーを要素に紐付け、関連する機能を制御するための JavaScript API です。シャドウ DOM ツリーはメイン文書の DOM とは別にレンダリングされるため、要素の内部構造やスタイルを外部から隔離できます。これにより、コンポーネントのスタイルがページの他の部分と衝突することを防げます。
element.shadowRoot 外部の JS から shadowRoot にアクセス可能。一般的な選択肢。
element.shadowRoot → null 外部からアクセス不可。ただしセキュリティ境界ではない点に注意。
class MyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* このスタイルは外部に影響しない */
:host { display: block; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
p { color: blue; font-weight: bold; }
</style>
<p><slot></slot></p>
`;
}
}
customElements.define('my-card', MyCard); :host シャドウホスト自身をスタイル :host(.active) 条件付きでホストをスタイル ::slotted(span) スロットに挿入された要素をスタイル ::part(header) 外部から shadow 内の part をスタイル 通常の CSS は Shadow DOM の内部に影響しませんが、CSS カスタムプロパティ(変数)は例外です。外部で定義した --my-color を Shadow DOM 内部で var(--my-color) として参照できます。これが Web Components のテーマ機能の基盤です。
:root {
--card-bg: #f8fafc; /* 外部で定義 */
--card-text: #1e293b;
}
/* Shadow DOM 内で参照可能 */
:host {
background: var(--card-bg);
color: var(--card-text);
} HTML Templates
レンダリングされないマークアップを定義し、JavaScript で再利用する仕組み
<template> と <slot> 要素によって、レンダリングされたページに表示されないマークアップのテンプレートを書くことができます。カスタム要素の構造の基礎として何度でも再利用でき、slot を使えば外部からコンテンツを差し込む柔軟な設計が可能です。
<template> HTML に書くが描画されない
.cloneNode(true) JS でテンプレートを複製
<slot> Shadow DOM に追加して描画
<slot> 名前なし。slot 属性を持たない全ての子要素を受け取る。
<slot name="header"> 対応する slot 属性を持つ要素のみを受け取る。
<!-- テンプレート定義 -->
<template id="card-template">
<style>
.card { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; }
.card-header { font-weight: bold; margin-bottom: 0.5rem; }
.card-footer { margin-top: 0.5rem; font-size: 0.85rem; color: #666; }
</style>
<div class="card">
<div class="card-header"><slot name="title">Default Title</slot></div>
<div><slot>Default content</slot></div>
<div class="card-footer"><slot name="footer"></slot></div>
</div>
</template> <!-- 使用: 複数のスロットにコンテンツを配置 -->
<my-card>
<span slot="title">Web Components Guide</span>
<p>Learn about Custom Elements, Shadow DOM, and Templates.</p>
<span slot="footer">Last updated: 2026</span>
</my-card> class MyCard extends HTMLElement {
connectedCallback() {
const template = document.getElementById('card-template');
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard); まとめ: 3 つを組み合わせた実践例
Custom Elements・Shadow DOM・HTML Templates の 3 つを組み合わせると、フレームワークに依存しない再利用可能なコンポーネントが完成します。以下は、プロフィールカードを Web Components で実装した例です。
Web Components デモ: <profile-card>
コードを編集して「実行」を押すと、右側のプレビューにリアルタイムで反映されます。スロットの内容やテーマ属性を変更してみてください。
この <profile-card> は React・Vue・Svelte などどのフレームワークからでも使用できます。フレームワークに依存しないため、デザインシステムの共通コンポーネントとして最適です。
対応ブラウザ
| 機能 | デスクトップ | モバイル | ||||
|---|---|---|---|---|---|---|
| Chrome | Edge | Firefox | Safari | Chrome Android | Safari iOS | |
| 26 | 13 | 22 | 8 | 26 | 8 | |
| DOM API | ||||||
| CustomElementRegistry インターフェイスは、カスタム要素の登録と、登録された要素を照会するためのメソッドを提供します。このインスタンスを取得するには、window.customElements プロパティを使用してください。 | 54 | 79 | 63 | 10.1 | 54 | 10.3 |
| define() は CustomElementRegistry インターフェイスのメソッドで、新しいカスタム要素を定義します。 | 54 | 79 | 63 | 10.1 | 54 | 10.3 |
define (disabledFeatures static property) 「disabledFeature」静的プロパティをサポートします | 77 | 79 | 92 | | 77 | |
| get() は CustomElementRegistry インターフェイスのメソッドで、以前定義したカスタム要素のコンストラクターを返します。 | 54 | 79 | 63 | 10.1 | 54 | 10.3 |
| CustomElementRegistry インターフェイスの getName() メソッドは、以前に定義されたカスタム要素の名前を返します。 | 117 | 117 | 116 | 17 | 117 | 17 |
| upgrade() は CustomElementRegistry インターフェイスのメソッドで、 Node サブツリー内のすべてのシャドウを含むカスタム要素を、メイン文書に接続する前であってもアップグレードします。 | 68 | 79 | 63 | 12.1 | 68 | 12.2 |
| whenDefined() は CustomElementRegistry インターフェイスのメソッドで、指定した名前の要素が定義されたときに解決されるプロミス (Promise) を返します。 | 54 | 79 | 63 | 10.1 | 54 | 10.3 |
| Element.attachShadow() メソッドは、シャドウ DOM ツリーを特定の要素に追加し、そのシャドウルート (ShadowRoot) への参照を返します。 | 53 | 79 | 63 | 10 | 53 | 10 |
| A boolean that specifies whether the shadow root is clonable: when set to true, the shadow host cloned with Node.cloneNode() or Document.importNode() will include shadow root in the copy. Its default value is false. | 124 | 124 | 123 | 17.4 | 124 | 17.4 |
| 論理値で、 true に設定された場合、フォーカス可能性に関するカスタム要素の問題を緩和します。シャドウ DOM のフォーカスができない部分がクリックされた場合、最初のフォーカス可能な部分がフォーカスを得て、シャドウホストは :focus のスタイルを利用することができます。既定値は false です。 | 53 | 79 | 94 | 13.1 | 53 | 13.4 |
| A boolean that, when set to true, indicates that the shadow root is serializable. If set, the shadow root may be serialized by calling the Element.getHTML() or ShadowRoot.getHTML() methods with the options.serializableShadowRoots parameter set true. Its default value is false. | 125 | 125 | 128 | 18 | 125 | 18 |
| Element.shadowRoot は読み取り専用のプロパティで、その要素がホストになっているシャドウルートを表します。 | 35 | 79 | 63 | 10 | 35 | 10 |
| composed は Event インターフェイスの読み取り専用プロパティで、イベントがシャドウ DOM 境界を越えて標準 DOM に伝播するかどうかを示す論理値を返すものです。 | 53 | 79 | 52 | 10 | 53 | 10 |
| composedPath() は Event インターフェイスのメソッドで、イベントの経路をリスナーが呼び出されるオブジェクトの配列で返します。シャドウルートが ShadowRoot.mode が closed の状態で作成された場合、シャドウツリーのノードは含まれません。 | 53 | 79 | 59 | 10 | 53 | 10 |
| HTMLTemplateElement インターフェイスは、HTML の template 要素の内容にアクセスできるようにします。 | 26 | 13 | 22 | 8 | 26 | 8 |
| HTMLTemplateElement.content プロパティは、 要素のテンプレートの内容 (DocumentFragment) を返します。 | 26 | 13 | 22 | 8 | 26 | 8 |
| getRootNode() は Node インターフェイスのメソッドで、そのコンテキストのオブジェクトのルート、利用できる場合はオプションでシャドウルートを含んだものを返します。 | 54 | 79 | 53 | 10.1 | 54 | 10.3 |
| isConnected は Node インターフェイスの読み出し専用のプロパティで、ノードが Document オブジェクトに(直接的または間接的に)接続されているかどうかを示す論理値を返します。 | 51 | 79 | 49 | 10 | 51 | 10 |
| ShadowRoot はシャドウ DOM API のインターフェイスで、文書の DOM ツリーから分離してレンダリングされた部分ツリーのルートノードを指します。 | 53 | 79 | 63 | 10 | 53 | 10 |
| clonable は ShadowRoot インターフェイスの読み取り専用プロパティで、シャドウルートが複製可能であれば true を返し、そうでなければ false を返します。 | 124 | 124 | 123 | 17.4 | 124 | 17.4 |
| delegatesFocus は ShadowRoot インターフェイスの読み取り専用プロパティで、シャドウルートがフォーカスを委任する場合は true、そうでなければ false を返します。 | 53 | 79 | 94 | 15 | 53 | 15 |
| host は ShadowRoot の読み取り専用プロパティで、 ShadowRoot が装着されている DOM 要素の参照を返します。 | 53 | 79 | 63 | 10 | 53 | 10 |
| mode は ShadowRoot の読み取り専用プロパティで、モードを open と closed のどちらかで示します。 これはシャドウルートの内部機能を JavaScript からアクセスできるかどうかを定義します。 | 53 | 79 | 63 | 10.1 | 53 | 10.3 |
| serializable は ShadowRoot インターフェイスの読み取り専用プロパティで、このシャドウルートがシリアライズ可能であれば、true を返します。 | 125 | 125 | 128 | 18 | 125 | 18 |
| customElements は Window インターフェイスの読み取り専用プロパティで、 CustomElementRegistry オブジェクトへのリファレンスを返します。これにより、新しいカスタム要素を登録したり、以前に登録したカスタム要素に関する情報を取得したりすることができます。 | 54 | 79 | 63 | 10.1 | 54 | 10.3 |
| その他 | ||||||
| `:defined` | 54 | 79 | 63 | 10 | 54 | 10 |
- このブラウザでは部分的にしか実装されていません
- 「自律カスタム要素」はサポートされますが、「カスタマイズされた組み込み要素」はサポートされません。 バグ 182671 を参照してください。
- このブラウザでは部分的にしか実装されていません
- 「自律カスタム要素」はサポートされますが、「カスタマイズされた組み込み要素」はサポートされません。 バグ 182671 を参照してください。
- このブラウザでは部分的にしか実装されていません
- 「自律カスタム要素」はサポートされますが、「カスタマイズされた組み込み要素」はサポートされません。
- このブラウザでは部分的にしか実装されていません
- 「自律カスタム要素」はサポートされますが、「カスタマイズされた組み込み要素」はサポートされません。
- 以前は別名で対応していました: cloneable (16.4)
- 以前は別名で対応していました: cloneable (16.4)
- Firefox 95 以前では、このプロパティは `<select>` 要素と `<input type='checkbox'>` 要素で誤って `false` に設定されていました。
- このバージョンで機能が削除されました (53)
- 以前は別名で対応していました: deepPath (50)
- このバージョンで機能が削除されました (53)
- 以前は別名で対応していました: deepPath (50)
アクセシビリティの注意点
Shadow DOM の内部コンテンツはスクリーンリーダーからアクセス可能ですが、カスタム要素名だけでは意味が伝わりません。内部にセマンティックな HTML 要素を配置し、適切な ARIA 属性を設定する必要があります。
<!-- カスタム要素名だけでは意味が伝わらない -->
<my-button>送信</my-button> スクリーンリーダーは <my-button> をどう扱うか分からない。ボタンとして認識されず、キーボード操作もできない。
<!-- 内部にセマンティック要素を配置 -->
<my-button>
#shadow-root
<button role="button" aria-label="送信">
<slot></slot>
</button>
</my-button> 内部に <button> を配置することで、スクリーンリーダーがボタンとして認識し、フォーカス・キーボード操作も自動的に有効になる。
- インタラクティブ要素には <button>、<a>、<input> 等のネイティブ要素を使う
- カスタム要素に適切な role 属性を設定する(role="tablist"、role="dialog" 等)
- aria-label / aria-describedby でコンテキスト情報を提供する
- キーボード操作(Tab / Enter / Space / Escape)を実装する
- フォーカス管理: Shadow DOM 内のフォーカス可能要素に delegatesFocus を検討する
セキュリティの観点
Shadow DOM はセキュリティ境界ではない
Shadow DOM はスタイルのカプセル化を提供するが、JavaScript からは element.shadowRoot でアクセス可能。機密データの隠蔽には使えない。
innerHTML への直接挿入の危険性
Custom Elements 内で innerHTML にユーザー入力を直接設定すると XSS 脆弱性が生じる。textContent や DOM API を使って安全にコンテンツを挿入すること。