Web Components のスロット設計術:命名付き slot/fallback/`slotchange` の実戦パターン
Web Components で再利用可能なコンポーネントを設計する際、スロット機構は欠かせない存在です。単に「穴を開けてコンテンツを挿入する」だけでなく、命名付き slot による柔軟な配置制御、fallback コンテンツによるデフォルト表示、そしてslotchangeイベントによる動的な制御まで、スロットには多彩な設計パターンが存在します。
本記事では、Web Components のスロット設計に焦点を当て、命名付き slot、fallback コンテンツ、slotchangeイベントの実戦的な活用パターンを解説します。初心者の方にもわかりやすく、段階的にコード例を示しながら説明していきますので、ぜひ最後までお読みください。
背景
Web Components は、HTML の標準技術としてカスタム要素を定義し、再利用可能なコンポーネントを構築できる仕組みです。その中核を担うのがShadow DOMとスロット機構になります。
Web Components におけるスロットの役割
スロットは、コンポーネントの外部から提供されたコンテンツを、コンポーネント内部の特定の位置に配置するための仕組みです。これにより、コンポーネントの構造を保ちながら、利用者が自由にコンテンツをカスタマイズできるようになります。
以下の図は、Web Components におけるスロットの基本的な仕組みを示しています。
mermaidflowchart TB
lightdom["Light DOM<br/>(利用者が提供)"]
shadowdom["Shadow DOM<br/>(コンポーネント内部)"]
slot["<slot> 要素"]
rendered["レンダリング結果"]
lightdom -->|投影| slot
slot -.->|内包| shadowdom
shadowdom -->|合成| rendered
lightdom -.->|表示| rendered
上図のように、Light DOM(通常の DOM)のコンテンツが Shadow DOM 内の<slot>要素に投影され、最終的なレンダリング結果として表示されるのです。
スロット機構の 3 つの柱
Web Components のスロット設計には、主に 3 つの重要な要素があります。
| # | 要素 | 役割 | 主な用途 |
|---|---|---|---|
| 1 | 命名付き slot | 複数のスロットを名前で識別 | ヘッダー、フッター、サイドバーなど配置を明示 |
| 2 | fallback コンテンツ | スロットが空の場合のデフォルト表示 | プレースホルダーや初期状態の提供 |
| 3 | slotchangeイベント | スロット内容の変化を検知 | 動的なレイアウト調整や状態管理 |
これらを組み合わせることで、柔軟で堅牢なコンポーネント設計が可能になります。
課題
Web Components のスロットを実際に設計する際、いくつかの課題に直面することがあります。
複数コンテンツの配置制御
単一のスロットだけでは、複数の異なる種類のコンテンツを適切な位置に配置することができません。たとえば、カード型コンポーネントでヘッダー、本文、フッターを別々に制御したい場合、どのように実装すればよいでしょうか。
デフォルト表示の提供
利用者がコンテンツを提供しなかった場合、空白のまま表示されてしまうと見栄えが悪くなります。適切なデフォルト表示を提供する方法が必要です。
動的なコンテンツ変化への対応
SPA やインタラクティブなアプリケーションでは、スロットに挿入されるコンテンツが動的に変化することがあります。その変化を検知して、レイアウトや状態を調整する仕組みが求められるのです。
以下の図は、スロット設計における典型的な課題を示しています。
mermaidflowchart TD
start["スロット設計開始"]
q1{"複数の配置<br/>エリアが必要?"}
q2{"デフォルト<br/>表示が必要?"}
q3{"動的な変化<br/>を検知?"}
problem1["課題1:配置制御"]
problem2["課題2:空状態対応"]
problem3["課題3:変化検知"]
start --> q1
q1 -->|はい| problem1
q1 -->|いいえ| q2
q2 -->|はい| problem2
q2 -->|いいえ| q3
q3 -->|はい| problem3
problem1 -.-> solution["解決策が必要"]
problem2 -.-> solution
problem3 -.-> solution
これらの課題を解決するためには、スロットの高度な機能を理解し、適切に活用する必要があります。
解決策
Web Components のスロット機構が提供する 3 つの主要機能を活用することで、前述の課題を解決できます。
命名付き slot による配置制御
命名付き slot(named slots)を使用すると、複数のスロットを名前で識別し、コンテンツを正確な位置に配置できます。
仕組み:
- Shadow DOM 内の
<slot>要素にname属性を指定 - Light DOM 側のコンテンツに
slot属性で対応する名前を指定 - 名前が一致するコンテンツが、対応するスロットに投影される
fallback コンテンツによるデフォルト表示
<slot>要素の内部にコンテンツを記述することで、スロットが空の場合に表示される fallback コンテンツを定義できます。
仕組み:
<slot>タグの開始と終了の間にデフォルトコンテンツを記述- Light DOM からコンテンツが提供されない場合、fallback が表示される
- コンテンツが提供されると、fallback は自動的に非表示になる
slotchangeイベントによる動的制御
slotchangeイベントをリスニングすることで、スロット内容の変化を検知し、適切な処理を実行できます。
仕組み:
<slot>要素でslotchangeイベントが発火- イベントハンドラー内で
assignedNodes()やassignedElements()を使用 - 投影されたノードの情報を取得し、動的に処理を実行
以下の図は、これら 3 つの解決策がどのように連携するかを示しています。
mermaidflowchart LR
component["カスタムコンポーネント"]
subgraph ShadowDOM["Shadow DOM"]
namedSlot["命名付きslot<br/>(name属性)"]
fallback["fallbackコンテンツ"]
listener["slotchangeリスナー"]
end
subgraph LightDOM["Light DOM"]
content["slot属性付き<br/>コンテンツ"]
end
content -->|投影| namedSlot
namedSlot -.->|空の場合| fallback
namedSlot -->|変化時| listener
component -.-> ShadowDOM
component -.-> LightDOM
これらの機能を組み合わせることで、柔軟で堅牢なスロット設計が実現できます。
具体例
ここからは、実際のコード例を通して、命名付き slot、fallback、slotchangeの実装パターンを段階的に見ていきましょう。
基本的な命名付き slot の実装
まずは、カード型コンポーネントで複数の命名付き slot を使用する基本的な例から始めます。
カスタム要素の定義
以下は、ヘッダー、本文、フッターの 3 つのスロットを持つカードコンポーネントの定義です。
typescript// カスタム要素クラスの定義
class CardComponent extends HTMLElement {
constructor() {
super();
// Shadow DOMをアタッチ
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// コンポーネントがDOMに追加されたときに呼ばれる
this.render();
}
Shadow DOM のテンプレート作成
次に、Shadow DOM 内部の構造を定義します。ここで命名付き slot を配置していきます。
typescript render() {
// Shadow DOMの構造を定義
this.shadowRoot.innerHTML = `
<style>
/* カードのスタイル定義 */
.card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
スロット領域のスタイリング
各スロットに対応する領域のスタイルを定義します。
typescript /* ヘッダー領域 */
.card-header {
background: #f5f5f5;
padding: 16px;
border-bottom: 1px solid #ddd;
}
/* 本文領域 */
.card-body {
padding: 16px;
}
/* フッター領域 */
.card-footer {
background: #f5f5f5;
padding: 12px 16px;
border-top: 1px solid #ddd;
font-size: 0.9em;
}
</style>
命名付き slot の配置
実際に HTML 構造内に命名付き slot を配置します。
typescript <div class="card">
<div class="card-header">
<!-- ヘッダー用の命名付きslot -->
<slot name="header"></slot>
</div>
<div class="card-body">
<!-- 本文用の命名付きslot(デフォルトslot) -->
<slot></slot>
</div>
<div class="card-footer">
<!-- フッター用の命名付きslot -->
<slot name="footer"></slot>
</div>
</div>
`;
}
}
カスタム要素の登録
定義したクラスをカスタム要素として登録します。
typescript// カスタム要素として登録
customElements.define('card-component', CardComponent);
HTML での使用例
実際に HTML 内でカスタム要素を使用する例です。slot属性で対応する命名付き slot を指定します。
html<!-- カード型コンポーネントの使用 -->
<card-component>
<!-- slot="header"でヘッダースロットに投影 -->
<h2 slot="header">商品情報</h2>
<!-- slot属性なしはデフォルトslotに投影 -->
<p>この商品は高品質な素材を使用しています。</p>
<p>価格:5,980円</p>
<!-- slot="footer"でフッタースロットに投影 -->
<button slot="footer">カートに追加</button>
</card-component>
上記のコードにより、ヘッダー、本文、フッターがそれぞれ適切な位置に配置されます。
fallback コンテンツの実装
次に、スロットが空の場合に表示される fallback コンテンツを追加します。
fallback 付きテンプレートの定義
以下は、各スロットに fallback コンテンツを定義した例です。
typescript render() {
this.shadowRoot.innerHTML = `
<style>
/* 前述のスタイルは省略 */
.placeholder {
color: #999;
font-style: italic;
}
</style>
<div class="card">
<div class="card-header">
<slot name="header">
<!-- ヘッダーのfallback -->
<span class="placeholder">タイトルなし</span>
</slot>
</div>
本文とフッターの fallback
本文とフッター部分にも fallback コンテンツを設定します。
typescript <div class="card-body">
<slot>
<!-- 本文のfallback -->
<p class="placeholder">コンテンツが提供されていません。</p>
</slot>
</div>
<div class="card-footer">
<slot name="footer">
<!-- フッターのfallback -->
<span class="placeholder">フッター情報なし</span>
</slot>
</div>
</div>
`;
}
fallback の動作確認
コンテンツを提供しない場合、fallback が表示されます。
html<!-- コンテンツを一部だけ提供 -->
<card-component>
<h2 slot="header">お知らせ</h2>
<!-- 本文とフッターは提供しない → fallbackが表示される -->
</card-component>
この場合、ヘッダーには「お知らせ」が表示され、本文には「コンテンツが提供されていません。」、フッターには「フッター情報なし」という fallback が表示されます。
slotchangeイベントの実装
最後に、スロット内容の変化を検知して動的に処理を行う実装を見ていきましょう。
イベントリスナーの設定
connectedCallback内で、各スロットにslotchangeイベントリスナーを設定します。
typescript connectedCallback() {
this.render();
// Shadow DOM内のすべてのslot要素を取得
const slots = this.shadowRoot.querySelectorAll('slot');
// 各slotにslotchangeイベントリスナーを追加
slots.forEach(slot => {
slot.addEventListener('slotchange', this.handleSlotChange.bind(this));
});
}
スロット変化時の処理
slotchangeイベントが発火したときの処理を定義します。
typescript handleSlotChange(event) {
// イベントが発生したslot要素を取得
const slot = event.target;
// 投影されたノードを取得
const assignedNodes = slot.assignedNodes({ flatten: true });
// テキストノードを除外して要素のみ取得
const assignedElements = slot.assignedElements();
console.log(`Slot "${slot.name || 'default'}" changed`);
console.log('Assigned nodes:', assignedNodes);
console.log('Assigned elements:', assignedElements);
}
動的なスタイル調整の実装
スロットの内容に応じて、動的にスタイルを調整する例です。
typescript handleSlotChange(event) {
const slot = event.target;
const assignedElements = slot.assignedElements();
// スロットが空の場合、親要素を非表示にする
const parent = slot.parentElement;
if (assignedElements.length === 0) {
parent.style.display = 'none';
} else {
parent.style.display = '';
}
}
この実装により、コンテンツが提供されていないスロットの領域を自動的に非表示にできます。
カウンター機能の追加
スロットに投影された要素の数をカウントして表示する機能を追加します。
typescript handleSlotChange(event) {
const slot = event.target;
const assignedElements = slot.assignedElements();
// カウンター表示用の要素を取得または作成
let counter = this.shadowRoot.querySelector('.item-counter');
if (!counter && slot.name === '') {
counter = document.createElement('div');
counter.className = 'item-counter';
this.shadowRoot.querySelector('.card-body').appendChild(counter);
}
// 要素数を表示
if (counter) {
counter.textContent = `アイテム数: ${assignedElements.length}`;
}
}
HTML での動的変化の例
JavaScript で動的にコンテンツを追加・削除すると、slotchangeイベントが発火します。
html<card-component id="dynamic-card">
<h2 slot="header">動的コンテンツ</h2>
<p>最初のアイテム</p>
</card-component>
<script>
const card = document.getElementById('dynamic-card');
// 2秒後に新しいアイテムを追加
setTimeout(() => {
const newItem = document.createElement('p');
newItem.textContent = '追加されたアイテム';
card.appendChild(newItem);
// slotchangeイベントが発火する
}, 2000);
</script>
実戦的な組み合わせパターン
ここまで学んだ 3 つの要素を組み合わせた、より実戦的なコンポーネントを実装します。
タブコンポーネントの実装
タブ切り替え機能を持つコンポーネントで、命名付き slot、fallback、slotchangeをすべて活用します。
typescriptclass TabsComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.activeTab = 0;
}
connectedCallback() {
this.render();
this.setupSlotListeners();
}
タブのテンプレート定義
タブのヘッダーとコンテンツ用のスロットを定義します。
typescript render() {
this.shadowRoot.innerHTML = `
<style>
.tabs-container {
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-headers {
display: flex;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.tab-content {
padding: 16px;
}
</style>
<div class="tabs-container">
<div class="tab-headers">
<slot name="tab-header">
<span class="placeholder">タブがありません</span>
</slot>
</div>
タブコンテンツ領域の定義
各タブのコンテンツを表示する領域を定義します。
typescript <div class="tab-content">
<slot name="tab-content">
<p class="placeholder">コンテンツがありません</p>
</slot>
</div>
</div>
`;
}
スロット変化の監視とタブ更新
slotchangeを利用して、タブの追加・削除を検知し、UI を更新します。
typescript setupSlotListeners() {
const headerSlot = this.shadowRoot.querySelector('slot[name="tab-header"]');
const contentSlot = this.shadowRoot.querySelector('slot[name="tab-content"]');
// タブヘッダーの変化を監視
headerSlot.addEventListener('slotchange', () => {
const headers = headerSlot.assignedElements();
console.log(`タブヘッダー数: ${headers.length}`);
this.updateTabState(headers);
});
// コンテンツの変化を監視
contentSlot.addEventListener('slotchange', () => {
const contents = contentSlot.assignedElements();
console.log(`タブコンテンツ数: ${contents.length}`);
});
}
タブ状態の更新処理
タブの数に応じて、適切な状態管理を行います。
typescript updateTabState(headers) {
if (headers.length === 0) {
// タブがない場合の処理
console.log('タブが削除されました');
} else if (this.activeTab >= headers.length) {
// アクティブなタブが範囲外の場合、最初のタブに戻す
this.activeTab = 0;
}
}
}
// カスタム要素として登録
customElements.define('tabs-component', TabsComponent);
以下の図は、実装したタブコンポーネントの動作フローを示しています。
mermaidsequenceDiagram
participant User as 利用者
participant LightDOM as Light DOM
participant Slot as スロット
participant Handler as slotchangeハンドラー
participant UI as UI更新
User->>LightDOM: タブ要素を追加
LightDOM->>Slot: コンテンツ投影
Slot->>Handler: slotchangeイベント発火
Handler->>Handler: assignedElements()で要素取得
Handler->>Handler: タブ数をカウント
Handler->>UI: タブヘッダー表示更新
UI->>User: 更新されたUIを表示
この実装パターンにより、タブの動的な追加・削除に対応した堅牢なコンポーネントが実現できます。
エラー処理とデバッグ
スロット実装時に遭遇しやすいエラーと、その対処法を紹介します。
よくあるエラー 1:slotchange が発火しない
エラーコード: なし(イベントが発火しない動作不具合)
エラーメッセージ:
textslotchangeイベントハンドラーが呼ばれない
コンソールにログが出力されない
発生条件:
- Shadow DOM がアタッチされる前にイベントリスナーを設定
mode: 'closed'で Shadow DOM を作成し、外部から参照できない
解決方法:
connectedCallback内でイベントリスナーを設定する- Shadow DOM は
mode: 'open'で作成する this.shadowRootが存在することを確認してから処理を実行
typescriptconnectedCallback() {
// Shadow DOMの存在確認
if (!this.shadowRoot) {
console.error('Shadow DOMが存在しません');
return;
}
this.render();
this.setupSlotListeners();
}
よくあるエラー 2:assignedNodes が空配列を返す
エラーコード: なし(期待しない動作)
エラーメッセージ:
textassignedNodes()またはassignedElements()が空配列[]を返す
スロットにコンテンツが投影されていないように見える
発生条件:
- Light DOM 側で
slot属性の名前が一致していない - スロット名のスペルミス
解決方法:
- Shadow DOM 内の
<slot name="xxx">と Light DOM 側のslot="xxx"が完全一致するか確認 - デフォルトスロット(名前なし)の場合、Light DOM 側も
slot属性を付けない - ブラウザの開発者ツールで実際の DOM 構造を確認
typescripthandleSlotChange(event) {
const slot = event.target;
const assignedElements = slot.assignedElements();
// デバッグ用ログ出力
console.log('Slot name:', slot.name || 'default');
console.log('Assigned elements count:', assignedElements.length);
if (assignedElements.length === 0) {
console.warn('このスロットには要素が投影されていません');
}
}
まとめ
Web Components のスロット設計において、命名付き slot、fallback コンテンツ、slotchangeイベントの 3 つの要素を適切に活用することで、柔軟で堅牢なコンポーネントを構築できます。
命名付き slotを使用することで、複数のコンテンツ領域を明示的に制御し、利用者にとってわかりやすい API を提供できます。ヘッダー、本文、フッターなど、役割が明確な領域には必ず名前を付けましょう。
fallback コンテンツは、コンポーネントの初期状態やエラー時の表示を改善します。ユーザーがコンテンツを提供しなかった場合でも、適切なプレースホルダーが表示されることで、UX が大きく向上するのです。
slotchangeイベントにより、動的なコンテンツ変化に対応できるようになります。SPA やインタラクティブなアプリケーションでは、この機能が特に重要です。
これらの機能を組み合わせることで、React や Vue.js などのフレームワークに依存せず、ブラウザ標準技術だけで高度なコンポーネント設計が可能になります。ぜひ本記事のパターンを参考に、実践的な Web Components の開発に挑戦してみてください。
関連リンク
articleWeb Components のスロット設計術:命名付き slot/fallback/`slotchange` の実戦パターン
articleWeb Components イベント設計チート:`CustomEvent`/`composed`/`bubbles` 実例集
articleWeb Components のポリフィル戦略:@webcomponents 系を最小限で入れる判断基準
articleWeb Components と Declarative Shadow DOM の現在地:HTML だけで描くサーバー発 UI
articleWeb Components のパッケージ配布戦略:types/CEM(Custom Elements Manifest)/ドキュメント自動
articleWeb Components が “is not a constructor” で落ちる時:定義順序と複数登録の衝突を解決
articleZod の再帰型・木構造設計:`z.lazy` でツリー/グラフを安全に表現
articleCline ガバナンス運用:ポリシー・承認フロー・監査証跡の整備
articleYarn の歴史と進化:Classic(v1) から Berry(v2/v4) まで一気に把握
articleClaude Code 中心の開発プロセス設計:要求 → 設計 → 実装 → 検証の最短動線
articleWeb Components のスロット設計術:命名付き slot/fallback/`slotchange` の実戦パターン
articleBun vs Node.js 徹底比較:起動時間・スループット・メモリの実測レポート
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来