Web Components 全体像を一枚図で理解する:Shadow DOM/slots/ElementInternals の関係

Web Components を学習する際、Shadow DOM、slots、ElementInternals という 3 つの核心技術を個別に理解することは重要ですが、それだけでは実際の開発で活用することは困難です。これらの技術は密接に連携し合い、統合的に動作することで真の価値を発揮します。
今回は、Web Components の全体像を一枚の図で理解し、3 つの技術がどのように関係し合って動作するのかを詳しく解説いたします。この統合的な理解により、より効率的で保守性の高いコンポーネント開発が可能になるでしょう。
背景
Web Components の 3 つの核心技術
モダンな Web 開発において、コンポーネント化は欠かせない設計手法となっています。Web Components は、ブラウザ標準でコンポーネント化を実現するための技術群であり、以下の 3 つの主要技術で構成されています。
以下の図は、Web Components の 3 つの核心技術とその役割を示しています。
mermaidflowchart TD
WC[Web Components] --> SD[Shadow DOM]
WC --> SL[slots]
WC --> EI[ElementInternals]
SD --> |カプセル化| Encap[スタイル・DOM隔離]
SL --> |コンテンツ配信| Content[外部コンテンツ挿入]
EI --> |状態管理| State[フォーム状態・アクセシビリティ]
style WC fill:#e1f5fe
style SD fill:#f3e5f5
style SL fill:#e8f5e8
style EI fill:#fff3e0
各技術が解決する課題は以下の通りです。
技術 | 主な課題 | 解決策 |
---|---|---|
Shadow DOM | スタイルの衝突、DOM 構造の露出 | カプセル化による隔離 |
slots | 固定的なコンテンツ構造 | 柔軟なコンテンツ配信 |
ElementInternals | フォーム連携、アクセシビリティ | 標準的な状態管理 |
それぞれの技術が解決する課題
Shadow DOMは、コンポーネント内部のスタイルと DOM 構造を外部から隔離することで、グローバルなスタイルの衝突を防ぎます。これにより、コンポーネントの独立性が保たれ、再利用性が大幅に向上するのです。
slotsは、コンポーネントの使用者が任意のコンテンツを挿入できる仕組みを提供します。テンプレート化されたコンポーネントに動的なコンテンツを配置することで、柔軟性と再利用性を両立できます。
ElementInternalsは、カスタム要素がブラウザのフォームシステムやアクセシビリティ機能と適切に連携するための標準的な API を提供します。これにより、ネイティブ要素と同等の機能を持つカスタム要素の開発が可能になります。
課題
各技術の個別理解だけでは不十分
Web Components の各技術を個別に学習することは重要ですが、実際の開発現場では以下のような課題に直面することがあります。
- 技術間の連携方法が不明確:どの技術をいつ、どのように組み合わせるべきかわからない
- 設計指針の欠如:全体的な設計パターンが見えないため、一貫性のないコンポーネントになりがち
- パフォーマンスへの影響:各技術の特性を理解せずに使用することで、不要な処理が発生する可能性
統合的な理解が必要な理由
現代の Web 開発では、ユーザー体験とメンテナンス性の両方を満たすコンポーネント設計が求められています。以下の理由から、統合的な理解が不可欠です。
開発効率の向上:3 つの技術の関係性を理解することで、設計段階から適切な技術選択ができ、後からの大幅な修正を避けられます。
コードの品質向上:各技術の役割分担を明確にすることで、責務の分離ができ、保守しやすいコードが書けるようになります。
チーム開発での一貫性:統合的な理解により、チームメンバー間で共通の設計指針を持つことができ、コードレビューや引き継ぎがスムーズになるでしょう。
解決策
Shadow DOM によるカプセル化
Shadow DOM は、コンポーネントの内部構造とスタイルを外部から隔離する基盤技術です。以下の図は、Shadow DOM のカプセル化メカニズムを示しています。
mermaidflowchart TB
subgraph "Light DOM(通常のDOM)"
LD[親要素]
CE[カスタム要素]
end
subgraph "Shadow DOM"
SR[Shadow Root]
SI[Shadow内部要素]
SS[Shadow内スタイル]
end
LD --> CE
CE -.->|attachShadow| SR
SR --> SI
SR --> SS
style SR fill:#f3e5f5
style SI fill:#f3e5f5
style SS fill:#f3e5f5
Shadow DOM の実装により、以下のメリットが得られます。
typescriptclass CustomButton extends HTMLElement {
constructor() {
super();
// Shadow Rootを作成してカプセル化を実現
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
/* このスタイルは外部に影響しない */
button {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
}
</style>
<button><slot></slot></button>
`;
}
}
このコードでは、Shadow DOM を作成することで、内部の button スタイルが外部のスタイルと衝突することを防いでいます。
slots によるコンテンツ配信
slots は、Shadow DOM 内に Light DOM のコンテンツを配信する仕組みです。以下の図で、slots のコンテンツフローを確認できます。
mermaidsequenceDiagram
participant User as 利用者
participant LE as Light DOM要素
participant SD as Shadow DOM
participant Slot as slot要素
User->>LE: コンテンツを挿入
LE->>SD: Shadow DOM内でslot検索
SD->>Slot: 対応するslotを特定
Slot->>SD: コンテンツを配置
SD->>User: 最終的な表示
named slot を使用した柔軟なコンテンツ配信の実装例:
typescriptclass CustomCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
}
.header { font-weight: bold; margin-bottom: 8px; }
.content { line-height: 1.5; }
</style>
<div class="card">
<div class="header">
<slot name="header">デフォルトヘッダー</slot>
</div>
<div class="content">
<slot>デフォルトコンテンツ</slot>
</div>
</div>
`;
}
}
この実装により、使用者は以下のように柔軟にコンテンツを配置できます。
html<custom-card>
<span slot="header">カスタムタイトル</span>
<p>カスタムコンテンツがここに表示されます。</p>
</custom-card>
ElementInternals による内部状態管理
ElementInternals は、カスタム要素がブラウザの標準機能と連携するための API です。フォーム連携とアクセシビリティの両方をサポートします。
typescriptclass CustomInput extends HTMLElement {
private internals: ElementInternals;
private input: HTMLInputElement;
static formAssociated = true; // フォーム連携を有効化
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.internals = this.attachInternals(); // ElementInternalsを取得
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
input {
border: 1px solid #ccc;
padding: 8px;
border-radius: 4px;
}
input:invalid {
border-color: #dc3545;
}
</style>
<input type="text" />
`;
this.input = this.shadowRoot.querySelector('input');
this.input.addEventListener('input', this.handleInput.bind(this));
}
ElementInternals を使用したバリデーション処理:
typescript private handleInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
// フォーム値の設定
this.internals.setFormValue(value);
// バリデーション
if (value.length < 3) {
this.internals.setValidity(
{ tooShort: true },
'3文字以上入力してください',
this.input
);
} else {
this.internals.setValidity({});
}
// アクセシビリティ情報の更新
this.internals.ariaLabel = `入力値: ${value}`;
}
}
この実装により、カスタム要素がネイティブフォーム要素と同様にブラウザの標準機能と連携できるようになります。
3 技術の相互作用パターン
3 つの技術は以下のように連携して動作します。
mermaidflowchart LR
subgraph "統合アーキテクチャ"
SD[Shadow DOM] -->|隔離環境提供| SL[slots]
SL -->|コンテンツ配信| EI[ElementInternals]
EI -->|状態通知| SD
SD -->|カプセル化| Benefit1[スタイル隔離]
SL -->|柔軟性| Benefit2[再利用性向上]
EI -->|標準連携| Benefit3[ユーザビリティ]
end
style SD fill:#f3e5f5
style SL fill:#e8f5e8
style EI fill:#fff3e0
この相互作用により、独立性、柔軟性、標準準拠という 3 つのメリットを同時に実現できます。
具体例
カスタムボタンコンポーネントの実装
実際のプロダクト開発で使用できる、本格的なカスタムボタンコンポーネントを実装してみましょう。このコンポーネントでは、3 つの技術がどのように連携するかを具体的に確認できます。
まず、基本的なクラス定義と Shadow DOM の設定を行います。
typescriptclass AdvancedButton extends HTMLElement {
private internals: ElementInternals;
private button: HTMLButtonElement;
static formAssociated = true;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.internals = this.attachInternals();
}
次に、Shadow DOM 内でのスタイルとテンプレート定義を実装します。
typescript connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
button {
background: var(--button-bg, #007bff);
color: var(--button-color, white);
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
button:hover {
background: var(--button-bg-hover, #0056b3);
transform: translateY(-1px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.icon {
font-size: 16px;
}
</style>
<button type="button">
<slot name="icon" class="icon"></slot>
<slot>ボタン</slot>
</button>
`;
this.button = this.shadowRoot.querySelector('button');
this.setupEventListeners();
}
イベントリスナーの設定と ElementInternals との連携を実装します。
typescript private setupEventListeners() {
this.button.addEventListener('click', this.handleClick.bind(this));
// disabled属性の監視
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'disabled') {
this.updateDisabledState();
}
});
});
observer.observe(this, { attributes: true });
this.updateDisabledState();
}
private handleClick(event: Event) {
if (this.hasAttribute('disabled')) {
event.preventDefault();
event.stopPropagation();
return;
}
// カスタムイベントの発火
this.dispatchEvent(new CustomEvent('button-click', {
detail: { timestamp: Date.now() },
bubbles: true
}));
// ElementInternalsによる状態更新
this.internals.ariaPressed = 'true';
setTimeout(() => {
this.internals.ariaPressed = 'false';
}, 100);
}
disabled 状態の管理とアクセシビリティ対応を実装します。
typescript private updateDisabledState() {
const isDisabled = this.hasAttribute('disabled');
this.button.disabled = isDisabled;
// ElementInternalsによるアクセシビリティ設定
this.internals.ariaDisabled = isDisabled ? 'true' : 'false';
if (isDisabled) {
this.internals.setValidity({ customError: true }, 'ボタンは無効です');
} else {
this.internals.setValidity({});
}
}
// 属性変更の監視
static get observedAttributes() {
return ['disabled', 'aria-label'];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (name === 'disabled') {
this.updateDisabledState();
} else if (name === 'aria-label') {
this.internals.ariaLabel = newValue;
}
}
}
customElements.define('advanced-button', AdvancedButton);
各技術がどう連携するかの実例
実装したボタンコンポーネントの使用例を以下に示します。
html<!DOCTYPE html>
<html>
<head>
<style>
/* カスタムプロパティによるテーマ設定 */
.primary {
--button-bg: #28a745;
--button-bg-hover: #218838;
}
.secondary {
--button-bg: #6c757d;
--button-bg-hover: #545b62;
}
</style>
</head>
<body>
<!-- アイコン付きボタン -->
<advanced-button
class="primary"
aria-label="保存ボタン"
>
<span slot="icon">💾</span>
保存
</advanced-button>
<!-- シンプルなボタン -->
<advanced-button class="secondary">
キャンセル
</advanced-button>
<!-- 無効状態のボタン -->
<advanced-button disabled>
無効なボタン
</advanced-button>
<script>
// イベントリスナーの設定
document.addEventListener('button-click', (event) => {
console.log(
'ボタンがクリックされました:',
event.detail
);
});
</script>
</body>
</html>
この実装例では、以下のように 3 つの技術が連携しています:
Shadow DOM:ボタンのスタイルと DOM 構造を外部から隔離し、CSS カスタムプロパティで外部からの制御を可能にしています。
slots:アイコンとテキストコンテンツを柔軟に配置でき、使用者のニーズに応じてカスタマイズできます。
ElementInternals:disabled 状態や aria 属性を適切に管理し、スクリーンリーダーなどの支援技術との連携を実現しています。
まとめ
統合的理解のメリット
Web Components の Shadow DOM、slots、ElementInternals という 3 つの技術を統合的に理解することで、以下のメリットが得られます。
開発効率の向上:各技術の役割と連携方法を理解することで、設計段階から適切な技術選択ができ、開発時間の短縮につながります。
コード品質の向上:責務の分離が明確になることで、保守しやすく拡張性の高いコンポーネントを作成できるようになります。
ユーザー体験の向上:アクセシビリティやパフォーマンスを考慮した実装が自然に行えるようになり、より良いユーザー体験を提供できます。
今回解説した全体像の理解を基に、ぜひ実際のプロジェクトで Web Components を活用してみてください。統合的なアプローチにより、モダンで持続可能な Web 開発が実現できるでしょう。
関連リンク
- article
Web Components の API 設計原則:属性 vs プロパティ vs メソッドの境界線
- article
Web Components スタイリング速見表:`::part`/`::slotted`/AdoptedStyleSheets(Constructable Stylesheets)
- article
Web Components を Vite + TypeScript + yarn で最短セットアップする完全手順
- article
Web Components 全体像を一枚図で理解する:Shadow DOM/slots/ElementInternals の関係
- article
Web Components Shadow DOM を使いこなす - スタイルカプセル化と Slot 活用テクニック
- article
Web Components Custom Elements の作り方完全ガイド - ライフサイクルから高度な機能まで
- article
Vitest `vi` API 技術チートシート:`mock` / `fn` / `spyOn` / `advanceTimersByTime` 一覧
- article
Pinia ストア分割テンプレ集:domain/ui/session の三層パターン
- article
Obsidian Markdown 拡張チートシート:Callout/埋め込み/内部リンク完全網羅
- article
Micro Frontends 設計:`vite-plugin-federation` で分割可能な UI を構築
- article
TypeScript 公開 API の型設計術:`export type`/`interface`/`class`の責務分担と境界設計
- article
Nuxt nuxi コマンド速見表:プロジェクト作成からモジュール公開まで
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来