12k
All articles

スロットを使用した柔軟なWebコンポーネントの構築

スロット、shadow DOM、名前付きスロットパターンを活用し、再利用可能なUIカードコンポーネントに構造化されたコンテンツを渡す柔軟なWebコンポーネントの構築方法を解説する。

OpenReplay Team
OpenReplay Team
スロットを使用した柔軟なWebコンポーネントの構築

Webコンポーネントは強力ですが、複雑なコンテンツを渡すことは、すぐに煩雑になる可能性があります。ヘッダー画像、タイトル、本文テキスト、アクションボタンが必要なカードコンポーネントを構築しようとする場合を想像してください。これらすべてを属性に詰め込むと、読みにくい混乱を招くことになります。そこでスロットの出番です。スロットは、柔軟で再利用可能なUIコンポーネントの構築方法を変革します。

この記事では、スロットを使用して、クリーンで宣言的なマークアップを維持しながらリッチなコンテンツを受け入れるWebコンポーネントを作成する方法を説明します。スロットがshadow DOMとどのように連携するか、デフォルトスロットと名前付きスロットの両方を実装する方法、そしてスロット化されたコンテンツを効果的にスタイリングする方法を学びます。

重要なポイント

  • スロットにより、Webコンポーネントは全てを属性に詰め込む代わりに、複雑なHTMLコンテンツを受け入れることができます
  • 名前付きスロットはコンテンツ配置の正確な制御を提供し、デフォルトスロットは未指定のコンテンツを処理します
  • ::slotted()疑似要素により、shadow DOM内からスロット化されたコンテンツのスタイリングが可能になります
  • スロットはDOMノードをコピーではなく投影することで、優れたパフォーマンスを維持します

問題:Webコンポーネントにおける複雑なコンテンツ

従来のHTML属性は、シンプルな値に対してはうまく機能します:

<user-avatar src="profile.jpg" size="large"></user-avatar>

しかし、構造化されたコンテンツを渡す必要がある場合はどうでしょうか?カードコンポーネントを考えてみましょう:

<!-- これはすぐに煩雑になります -->
<product-card 
  title="Premium Headphones"
  description="<p>High-quality audio with <strong>noise cancellation</strong></p>"
  price="$299"
  button-text="Add to Cart"
  image-src="headphones.jpg">
</product-card>

このアプローチには以下のような問題があります:

  • 属性内のHTMLはエスケープが必要
  • 複雑なレイアウトの管理が不可能になる
  • コンポーネントの使用方法が標準的なHTMLパターンと一致しない

スロットの仕組み:基本

スロットを使用すると、ネイティブHTML要素と同様に、コンポーネントのタグ間にコンテンツを直接渡すことができます。前の例がどのように変わるかを見てみましょう:

<product-card>
  <h2 slot="title">Premium Headphones</h2>
  <div slot="description">
    <p>High-quality audio with <strong>noise cancellation</strong></p>
  </div>
  <button slot="action">Add to Cart</button>
</product-card>

Webコンポーネント内では、<slot>要素を使用してこのコンテンツがどこに表示されるかを定義します:

class ProductCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
        }
      </style>
      <div class="card">
        <slot name="title">Untitled Product</slot>
        <slot name="description">No description available</slot>
        <slot name="action"></slot>
      </div>
    `;
  }
}

customElements.define('product-card', ProductCard);

<slot>タグ内のコンテンツはフォールバックコンテンツとして機能します。一致するスロット化されたコンテンツが提供されない場合に表示されます。

デフォルトスロット vs 名前付きスロット

Webコンポーネントは2種類のスロットをサポートします:

デフォルトスロット

slot属性を持たないコンテンツは、デフォルト(無名)スロットに入ります:

// コンポーネント定義
shadow.innerHTML = `
  <article>
    <h2>Article Title</h2>
    <slot></slot> <!-- デフォルトスロット -->
  </article>
`;
<!-- 使用方法 -->
<my-article>
  <p>この段落はデフォルトスロットに入ります</p>
  <p>これも同様です</p>
</my-article>

名前付きスロット

名前付きスロットは、コンテンツ配置の正確な制御を提供します:

// コンポーネント定義
shadow.innerHTML = `
  <div class="profile">
    <slot name="avatar"></slot>
    <div class="info">
      <slot name="name">Anonymous</slot>
      <slot name="bio">No bio provided</slot>
    </div>
  </div>
`;
<!-- 使用方法 -->
<user-profile>
  <img slot="avatar" src="jane.jpg" alt="Jane">
  <h3 slot="name">Jane Developer</h3>
  <p slot="bio">Building amazing web components</p>
</user-profile>

実際の例:柔軟なカードコンポーネントの構築

スロットの実用性を実証する、本格的なカードコンポーネントを構築してみましょう:

class FlexCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #e0e0e0;
          border-radius: 8px;
          overflow: hidden;
          background: white;
        }
        
        .header {
          padding: 16px;
          border-bottom: 1px solid #e0e0e0;
        }
        
        .content {
          padding: 16px;
        }
        
        .footer {
          padding: 16px;
          background: #f5f5f5;
        }
        
        /* スロット化されたコンテンツがない場合は空のセクションを非表示 */
        .header:empty {
          display: none;
        }
        
        .footer:empty {
          display: none;
        }
      </style>
      
      <div class="header">
        <slot name="header"></slot>
      </div>
      <div class="content">
        <slot></slot>
      </div>
      <div class="footer">
        <slot name="footer"></slot>
      </div>
    `;
  }
}

customElements.define('flex-card', FlexCard);

これで、任意のコンテンツ構造で使用できます:

<flex-card>
  <h2 slot="header">Product Details</h2>
  
  <p>メインコンテンツはデフォルトスロットに入ります</p>
  <ul>
    <li>機能 1</li>
    <li>機能 2</li>
  </ul>
  
  <div slot="footer">
    <button>今すぐ購入</button>
    <button>後で保存</button>
  </div>
</flex-card>

スロット化されたコンテンツのスタイリング

スロット化されたコンテンツのスタイリングには、特別な疑似要素が必要です:

::slotted()の使用

::slotted()疑似要素は、スロットに配置された要素をターゲットにします:

/* コンポーネントのshadow DOM内 */
::slotted(h2) {
  color: #333;
  margin: 0;
}

::slotted(button) {
  background: #007bff;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
}

/* 特定のスロットをターゲット */
::slotted([slot="header"]) {
  font-size: 1.2em;
}

重要な制限: ::slotted()は直接スロット化された要素のみをターゲットにし、その子要素はターゲットにしません。

:hostの使用

:host疑似クラスは、コンポーネント自体をスタイリングします:

:host {
  display: block;
  margin: 16px 0;
}

/* 属性に基づくスタイル */
:host([variant="primary"]) {
  border-color: #007bff;
}

/* コンテキストに基づくスタイル */
:host-context(.dark-theme) {
  background: #333;
  color: white;
}

パフォーマンスに関する考慮事項

スロットは、DOMノードをコピーするのではなく投影するため、非常に高いパフォーマンスを発揮します。スロット化されたコンテンツはlight DOM内に残りますが、shadow DOMの一部であるかのようにレンダリングされます。これは以下を意味します:

  • スロット化されたコンテンツのイベントリスナーは引き続き機能する
  • ドキュメントからのスタイルは引き続き適用される(shadow DOMによってブロックされない限り)
  • ブラウザはメモリ内でノードを複製しない

ブラウザサポートとポリフィル

Webコンポーネントのスロットはモダンブラウザでは優れたサポートを持っています。古いブラウザについては、Web Components polyfillsの使用を検討してください。

まとめ

スロットは、Webコンポーネントをシンプルなカスタム要素から、強力で柔軟な再利用可能UIの構築ブロックに変革します。構造をコンテンツから分離することで、高度にカスタマイズ可能でありながら使いやすいコンポーネントを作成できます。デザインシステムを構築している場合でも、コードをより良く整理している場合でも、スロットの習得は現代のWebコンポーネント開発に不可欠です。

より柔軟なWebコンポーネントを作成する準備はできましたか?既存のコンポーネントの1つをスロットを使用するようにリファクタリングすることから始めてください。現在属性を通じてHTMLを渡している領域や、複雑なプロパティ構造を使用している領域に焦点を当ててください。将来のあなた(そしてあなたのチーム)は、よりクリーンで保守しやすいコードに感謝するでしょう。

よくある質問

Webコンポーネントにおけるスロットとプロパティの違いは何ですか?

プロパティ(属性)は、文字列、数値、ブール値などのシンプルな値に最適です。スロットは複雑なHTMLコンテンツ、複数の要素、または任意のマークアップ構造を受け入れることに優れています。設定にはプロパティを、コンテンツにはスロットを使用してください。

JavaScriptでスロットコンテンツを動的に変更できますか?

はい、スロット化されたコンテンツはlight DOM内に残るため、いつでも変更できます。slot属性を持つ要素を選択し、他のDOM要素と同様に更新するだけです。変更はレンダリングされたコンポーネントに即座に反映されます。

スロットはSEOとアクセシビリティにどのような影響を与えますか?

スロット化されたコンテンツはlight DOM内に残るため、検索エンジンやスクリーンリーダーから完全にアクセス可能です。これは、クローラーがインデックス化するのが困難な場合があるshadow DOMコンテンツに対する大きな利点です。

同じ名前付きスロットに複数の要素を割り当てるとどうなりますか?

同じスロット名を持つすべての要素は、ドキュメント順でそのスロットに表示されます。これは、ユーザーが単一のスロット領域に複数のアイテムを追加できる柔軟なレイアウトを作成するのに便利です。

shadow DOMなしでスロットを使用できますか?

いいえ、スロットが機能するにはshadow DOMが必要です。スロットは、light DOMコンテンツをshadow DOMテンプレートに投影するように特別に設計されています。shadow DOMなしでは、異なるコンテンツ配布パターンを使用する必要があります。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.