Shadow DOM 201

CSS とスタイル設定

この記事では、Shadow DOM でできる素晴らしいことについて詳しく説明します。このドキュメントは、Shadow DOM 101 で説明されているコンセプトに基づいています。概要については、こちらの記事をご覧ください。

はじめに

実際のところ、スタイルのないマークアップは、見た目がよくありません。幸いなことに、ウェブ コンポーネントの背後にいる優秀な人々は、このことを予見しており、私たちを放置しませんでした。CSS Scoping Module では、シャドウツリー内のコンテンツのスタイルを設定するための多くのオプションが定義されています。

スタイルのカプセル化

Shadow DOM のコア機能の一つは、シャドウ境界です。多くの優れた特性がありますが、その中でも特に優れているのは、スタイルのカプセル化を無料で提供していることです。別の言い方をすると、次のようになります。

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

このデモには、2 つの興味深い点があります。

  • このページには他にも h3 がありますが、h3 セレクタに一致し、赤色にスタイル設定されているのは ShadowRoot 内の h3 のみです。ここでも、デフォルトでスコープ付きスタイルが使用されます。
  • このページで定義されている h3 をターゲットとする他のスタイルルールは、コンテンツに影響しません。これは、セレクタがシャドウ境界を越えないためです。

この話の教訓は、外部からスタイルがカプセル化されています。Shadow DOM のおかげです。

ホスト要素のスタイル設定

:host を使用すると、シャドウツリーをホストする要素を選択してスタイルを設定できます。

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

注意点として、親ページのルールは、要素で定義された :host ルールよりも優先度が高くなりますが、ホスト要素で定義された style 属性よりも優先度が低くなります。これにより、ユーザーは外部からスタイル設定をオーバーライドできます。:host は ShadowRoot のコンテキストでのみ機能するため、Shadow DOM の外部では使用できません。

:host(<selector>) の関数形式を使用すると、<selector> に一致する場合にホスト要素をターゲットにできます。

例 - 要素自体にクラス .different がある場合にのみ一致(例: <x-foo class="different"></x-foo>):

:host(.different) {
    ...
}

ユーザーの状態への対応

:host の一般的なユースケースは、カスタム要素を作成し、さまざまなユーザーの状態(:hover、:focus、:active など)に対応する場合です。

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

要素のテーマ設定

:host-context(<selector>) 疑似クラスは、ホスト要素またはその祖先要素のいずれかが <selector> と一致する場合に、ホスト要素と一致します。

:host-context() の一般的な用途は、周囲の要素に基づいて要素のテーマを設定することです。たとえば、多くのユーザーは、<html> または <body> にクラスを適用してテーマ設定を行います。

<body class="different">
  <x-foo></x-foo>
</body>

:host-context(.different) を使用すると、.different クラスを持つ要素の子孫である場合に <x-foo> のスタイルを設定できます。

:host-context(.different) {
  color: red;
}

これにより、コンテキストに基づいて一意にスタイルを設定する要素の Shadow DOM にスタイルルールをカプセル化できます。

1 つのシャドウルート内から複数のホストタイプをサポートする

:host のもう 1 つの用途は、テーマ設定ライブラリを作成し、同じ Shadow DOM 内からさまざまなタイプのホスト要素のスタイル設定をサポートする場合です。

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

外部から Shadow DOM 内部のスタイルを設定する

::shadow 疑似要素と /deep/ 結合子は、CSS 権限のヴォーパルソードのようなものです。これらを使用すると、Shadow DOM の境界を越えて、シャドウツリー内の要素にスタイルを設定できます。

::shadow 疑似要素

要素に 1 つ以上のシャドウツリーがある場合、::shadow 疑似要素はシャドウルート自体と一致します。これにより、要素のシャドウ DOM の内部にあるノードのスタイルを設定するセレクタを記述できます。

たとえば、要素がシャドウルートをホストしている場合、#host::shadow span {} を記述して、シャドウツリー内のすべてのスパンのスタイルを設定できます。

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

例(カスタム要素)- <x-tabs> の Shadow DOM には <x-panel> 個の子要素があります。各パネルは、h2 見出しを含む独自のシャドウツリーをホストします。メインページからこれらの見出しのスタイルを設定するには、次のように記述します。

x-tabs::shadow x-panel::shadow h2 {
    ...
}

/deep/ 結合子

/deep/ コンビネータは ::shadow に似ていますが、より強力です。すべてのシャドウ境界を完全に無視し、任意の数のシャドウツリーを横断します。簡単に言うと、/deep/ を使用すると、要素の内部をドリルダウンして任意のノードをターゲットに設定できます。

/deep/ 結合子は、複数のレベルの Shadow DOM が一般的であるカスタム要素の世界で特に役立ちます。主な例としては、多数のカスタム要素(それぞれが独自のシャドウツリーをホストする)をネストすることや、<shadow> を使用して別の要素から継承する要素を作成することが挙げられます。

例(カスタム要素)- ツリー内の任意の場所にある <x-tabs> の子孫であるすべての <x-panel> 要素を選択します。

x-tabs /deep/ x-panel {
    ...
}

例 - シャドウツリー内の任意の場所で、クラス .library-theme を持つすべての要素にスタイルを設定します。

body /deep/ .library-theme {
    ...
}

querySelector() の操作

.shadowRoot が DOM トラバーサルのためにシャドウツリーを開くのと同様に、コンビネータはセレクタ トラバーサルのためにシャドウツリーを開きます。複雑なネストされたチェーンを記述する代わりに、次のように 1 つのステートメントを記述できます。

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

ネイティブ要素のスタイル設定

ネイティブ HTML コントロールのスタイル設定は困難です。多くの人が諦めて、独自に開発しています。ただし、::shadow と /deep/ を使用すると、Shadow DOM を使用するウェブ プラットフォームの任意の要素にスタイルを設定できます。<input> 型と <video> がその好例です。

video /deep/ input[type="range"] {
  background: hotpink;
}

スタイル フックの作成

カスタマイズは便利ですが、場合によっては、Shadow のスタイル設定シールドに穴を開けて、他のユーザーがスタイルを設定するためのフックを作成したいことがあります。

::shadow と /deep/ の使用

/deep/ には多くの機能が備わっています。これにより、コンポーネントの作成者は、個々の要素をスタイル設定可能として指定したり、一連の要素をテーマ設定可能として指定したりできます。

例 - すべてのシャドウツリーを無視して、クラス .library-theme を持つすべての要素のスタイルを設定します。

body /deep/ .library-theme {
    ...
}

カスタム疑似要素の使用

WebKit と Firefox の両方で、ネイティブ ブラウザ要素の内部部分のスタイルを設定するための疑似要素が定義されています。わかりやすい例は input[type=range] です。スライダーのつまみ <span style="color:blue">blue</span> のスタイルを設定するには、::-webkit-slider-thumb をターゲットにします。

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

ブラウザが一部の内部にスタイル設定フックを提供するのと同様に、Shadow DOM コンテンツの作成者は、特定の要素を外部からスタイル設定可能として指定できます。これは、カスタム疑似要素を使用して行います。

pseudo 属性を使用して、要素をカスタム疑似要素として指定できます。その値(名前)には、接頭辞「x-」を付ける必要があります。これにより、シャドウツリー内のその要素との関連付けが作成され、外部の要素がシャドウ境界を越えるための指定されたレーンが提供されます。

カスタム スライダー ウィジェットを作成し、スライダーのつまみを青色にスタイル設定できるようにする例を次に示します。

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

CSS 変数の使用

テーマ設定フックを作成する効果的な方法は、CSS 変数を使用することです。つまり、他のユーザーが入力するための「スタイル プレースホルダ」を作成します。

カスタム要素の作成者が、Shadow DOM で変数のプレースホルダをマークアウトするとします。1 つは内部ボタンのフォントのスタイル設定用、もう 1 つは色のスタイル設定用です。

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

次に、要素の埋め込み側が、それらの値を好みに合わせて定義します。おそらく、独自のページの超クールな Comic Sans テーマに合わせるためです。

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

CSS 変数の継承方法により、すべてがうまく機能します。全体像は次のようになります。

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

スタイルをリセットする

フォント、色、行の高さなどの継承可能なスタイルは、Shadow DOM 内の要素に引き続き影響します。ただし、最大限の柔軟性を実現するために、Shadow DOM にはシャドウ境界で何が起こるかを制御する resetStyleInheritance プロパティが用意されています。新しいコンポーネントを作成する際に、最初からやり直す方法と考えてください。

resetStyleInheritance

  • false - デフォルト。継承可能な CSS プロパティは引き続き継承されます。
  • true - 境界で継承可能なプロパティを initial にリセットします。

以下は、resetStyleInheritance を変更することでシャドウツリーがどのように影響を受けるかを示すデモです。

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
DevTools の継承されたプロパティ

.resetStyleInheritance の理解は少し複雑です。これは主に、継承可能な CSS プロパティにのみ影響するためです。これは、継承するプロパティを探すときに、ページと ShadowRoot の境界で、ホストから値を継承するのではなく、代わりに initial 値を使用する(CSS 仕様に準拠)ことを意味します。

CSS で継承されるプロパティが不明な場合は、こちらの便利なリストを確認するか、[Elements] パネルの [Show inherited] チェックボックスをオンにしてください。

分散型ノードのスタイル設定

分散ノードは、挿入ポイント(<content> 要素)でレンダリングされる要素です。<content> 要素を使用すると、Light DOM からノードを選択し、Shadow DOM の事前定義された場所にレンダリングできます。論理的には Shadow DOM には含まれず、ホスト要素の子のままです。挿入ポイントはレンダリングにのみ関係します。

分散ノードは、メイン ドキュメントのスタイルを保持します。つまり、メインページのスタイルルールは、挿入ポイントでレンダリングされる場合でも、要素に引き続き適用されます。繰り返しになりますが、分散ノードは論理的にはライト DOM にあり、移動しません。別の場所でレンダリングされるだけです。ただし、ノードが Shadow DOM に分散されると、シャドウツリー内で定義された追加のスタイルが適用されることがあります。

::content 疑似要素

分散ノードはホスト要素の子要素です。では、Shadow DOM の内部から分散ノードをターゲットにするにはどうすればよいでしょうか。答えは、CSS の ::content 疑似要素です。これは、挿入ポイントを通過する Light DOM ノードをターゲットにする方法です。次に例を示します。

::content > h3 は、挿入ポイントを通過する h3 タグのスタイルを設定します。

例を見てみましょう。

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

挿入ポイントでのスタイルのリセット

ShadowRoot を作成するときに、継承されたスタイルをリセットするオプションがあります。<content> と <shadow> の挿入ポイントにもこのオプションがあります。これらの要素を使用する場合は、JS で .resetStyleInheritance を設定するか、要素自体にブール値の reset-style-inheritance 属性を使用します。

  • ShadowRoot または <shadow> 挿入ポイントの場合: reset-style-inheritance は、継承可能な CSS プロパティが、シャドウ コンテンツに到達する前にホストで initial に設定されることを意味します。この位置は上部境界と呼ばれます。

  • <content> 挿入ポイントの場合: reset-style-inheritance は、ホストの子が挿入ポイントに分散される前に、継承可能な CSS プロパティが initial に設定されることを意味します。この位置は下限と呼ばれます。

まとめ

カスタム要素の作成者には、コンテンツのルック アンド フィールを制御するためのオプションが多数用意されています。Shadow DOM は、この素晴らしい新世界の基盤を形成しています。

Shadow DOM は、スコープ付きのスタイル カプセル化と、外部の世界をどの程度取り込むかを選択する手段を提供します。カスタム疑似要素を定義するか、CSS 変数プレースホルダを含めることで、作成者はサードパーティに便利なスタイル設定フックを提供して、コンテンツをさらにカスタマイズできます。全体として、ウェブ作成者はコンテンツの表示方法を完全に制御できます。