2021年5月17日

Shadow DOM のスタイリング

Shadow DOM には、<style> タグと <link rel="stylesheet" href="…"> タグの両方を含めることができます。後者の場合、スタイルシートは HTTP キャッシュされるため、同じテンプレートを使用する複数のコンポーネントに対して再ダウンロードされることはありません。

一般的に、ローカルスタイルはシャドウツリー内でのみ機能し、ドキュメントスタイルはシャドウツリーの外側で機能します。ただし、いくつかの例外があります。

:host

:host セレクターを使用すると、シャドウホスト(シャドウツリーを含む要素)を選択できます。

たとえば、中央揃えにする必要がある <custom-dialog> 要素を作成しています。そのためには、<custom-dialog> 要素自体をスタイル設定する必要があります。

まさにそれが :host の役割です。

<template id="tmpl">
  <style>
    /* the style will be applied from inside to the custom-dialog element */
    :host {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>
</template>

<script>
customElements.define('custom-dialog', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
  }
});
</script>

<custom-dialog>
  Hello!
</custom-dialog>

カスケード

シャドウホスト(<custom-dialog> 自体)はライトDOM に存在するため、ドキュメントの CSS ルールによって影響を受けます。

:host でローカルに、そしてドキュメント内で両方スタイル設定されているプロパティがある場合、ドキュメントスタイルが優先されます。

たとえば、ドキュメントに以下があった場合

<style>
custom-dialog {
  padding: 0;
}
</style>

<custom-dialog> にはパディングがなくなります。

これは非常に便利です。ドキュメントで簡単に上書きできる「デフォルト」コンポーネントスタイルを :host ルールで設定できます。

例外は、ローカルプロパティに !important が付いている場合です。このようなプロパティでは、ローカルスタイルが優先されます。

:host(selector)

:host と同じですが、シャドウホストが selector と一致する場合にのみ適用されます。

たとえば、centered 属性を持つ場合にのみ <custom-dialog> を中央揃えにしたいとします。

<template id="tmpl">
  <style>
    :host([centered]) {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      border-color: blue;
    }

    :host {
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>
</template>

<script>
customElements.define('custom-dialog', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
  }
});
</script>


<custom-dialog centered>
  Centered!
</custom-dialog>

<custom-dialog>
  Not centered.
</custom-dialog>

これで、追加の中央揃えスタイルは最初のダイアログ <custom-dialog centered> にのみ適用されます。

要約すると、:host 系のセレクターを使用して、コンポーネントの主要な要素をスタイル設定できます。これらのスタイル(!important を除く)は、ドキュメントによって上書きできます。

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

それでは、スロットの場合を考えてみましょう。

スロットされた要素はライトDOM から来るため、ドキュメントスタイルを使用します。ローカルスタイルはスロットされたコンテンツには影響しません。

以下の例では、ドキュメントスタイルに従ってスロットされた <span> はボールドになっていますが、ローカルスタイルの background は適用されません。

<style>
  span { font-weight: bold }
</style>

<user-card>
  <div slot="username"><span>John Smith</span></div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
      span { background: red; }
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

結果はボールドですが、赤ではありません。

コンポーネント内のスロットされた要素をスタイル設定したい場合、2つの選択肢があります。

まず、<slot> 自体をスタイル設定し、CSS の継承に依存する方法があります。

<user-card>
  <div slot="username"><span>John Smith</span></div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
      slot[name="username"] { font-weight: bold; }
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

ここでは、CSS の継承が <slot> とその内容の間で有効であるため、<p>John Smith</p> はボールドになります。ただし、CSS 自体では、すべてのプロパティが継承されるわけではありません。

もう1つの方法は、::slotted(selector)擬似クラスを使用することです。これは2つの条件に基づいて要素に一致します。

  1. それはライトDOM から来るスロットされた要素です。スロット名は関係ありません。単にスロットされた要素ですが、その子要素ではなく、要素自体のみです。
  2. 要素は selector と一致します。

この例では、::slotted(div)<div slot="username"> を正確に選択しますが、その子要素は選択しません。

<user-card>
  <div slot="username">
    <div>John Smith</div>
  </div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
      ::slotted(div) { border: 1px solid red; }
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

ご注意ください。::slotted セレクターは、スロット内をさらに深く降りることはできません。これらのセレクターは無効です。

::slotted(div span) {
  /* our slotted <div> does not match this */
}

::slotted(div) p {
  /* can't go inside light DOM */
}

また、::slotted は CSS でのみ使用できます。querySelector では使用できません。

カスタムプロパティを使用した CSS フック

メインドキュメントからコンポーネントの内部要素をどのようにスタイル設定しますか?

:host のようなセレクターは <custom-dialog> 要素または <user-card> にルールを適用しますが、内部の Shadow DOM 要素をどのようにスタイル設定しますか?

ドキュメントから Shadow DOM スタイルに直接影響を与えるセレクターはありません。しかし、コンポーネントと対話するためのメソッドを公開するのと同じように、CSS 変数(カスタム CSS プロパティ)を公開してスタイル設定できます。

カスタム CSS プロパティは、ライトとシャドウの両方のすべてのレベルに存在します。

たとえば、Shadow DOM では --user-card-field-color CSS 変数を使用してフィールドをスタイル設定し、外部ドキュメントでその値を設定できます。

<style>
  .field {
    color: var(--user-card-field-color, black);
    /* if --user-card-field-color is not defined, use black color */
  }
</style>
<div class="field">Name: <slot name="username"></slot></div>
<div class="field">Birthday: <slot name="birthday"></slot></div>

次に、<user-card> の外部ドキュメントでこのプロパティを宣言できます。

user-card {
  --user-card-field-color: green;
}

カスタム CSS プロパティは Shadow DOM を貫通するため、どこにでも表示されます。そのため、内部の .field ルールがそれを利用します。

完全な例を以下に示します。

<style>
  user-card {
    --user-card-field-color: green;
  }
</style>

<template id="tmpl">
  <style>
    .field {
      color: var(--user-card-field-color, black);
    }
  </style>
  <div class="field">Name: <slot name="username"></slot></div>
  <div class="field">Birthday: <slot name="birthday"></slot></div>
</template>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true));
  }
});
</script>

<user-card>
  <span slot="username">John Smith</span>
  <span slot="birthday">01.01.2001</span>
</user-card>

要約

Shadow DOM には、<style><link rel="stylesheet"> などのスタイルを含めることができます。

ローカルスタイルは以下に影響を与えます。

  • シャドウツリー、
  • :host および :host() 擬似クラスを使用したシャドウホスト、
  • スロットされた要素(ライトDOM から来るもの)、::slotted(selector) を使用すると、スロットされた要素自体を選択できますが、その子要素は選択できません。

ドキュメントスタイルは以下に影響を与えます。

  • シャドウホスト(外部ドキュメントに存在するため)
  • スロットされた要素とその内容(これも外部ドキュメント内にあるため)

CSS プロパティが競合する場合、通常はドキュメントスタイルが優先されます。ただし、プロパティが !important としてラベル付けされている場合を除きます。その場合は、ローカルスタイルが優先されます。

カスタム CSS プロパティは Shadow DOM を貫通します。これらは、コンポーネントをスタイル設定するための「フック」として使用されます。

  1. コンポーネントは、カスタム CSS プロパティを使用して、var(--component-name-title, <default value>) のような主要な要素をスタイル設定します。
  2. コンポーネント作成者は、これらのプロパティを開発者向けに公開します。これらは、他の公開コンポーネントメソッドと同じくらい重要です。
  3. 開発者がタイトルのスタイルを設定したい場合、シャドウホストまたはその上の --component-name-title CSS プロパティを割り当てます。
  4. 完了!
チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案がある場合は、コメントする代わりに GitHub の issue またはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには <code> タグを使用し、複数行の場合は <pre> タグで囲み、10 行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。