Shadow DOM 201

CSS とスタイル

この記事では、Shadow DOM の優れた機能について詳しく説明します。Shadow DOM 101 で説明されているコンセプトに基づいています。概要については、こちらの記事をご覧ください。

はじめに

実際のところ、スタイルのないマークアップには、特別なことは一切ありません。幸いなことに、Web Components の開発の優秀なチームはこれを予測し、私たちを悩ませませんでした。CSS Scoping Module は、Shadow ツリー内のコンテンツをスタイル設定するための多くのオプションを定義しています。

スタイルのカプセル化

Shadow DOM のコア機能の一つは、Shadow 境界です。多くの優れたプロパティがありますが、特に優れているのは、スタイルのカプセル化が無料であることです。別の言い方をすると、

<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>

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

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

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

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

:host のもう一つの用途は、テーマ設定ライブラリを作成し、同じ 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 の権威を持つ Vorpal の剣のようなものです。これによって、Shadow DOM の境界を突破して Shadow ツリー内の要素のスタイル設定が可能になります。

::shadow 疑似要素

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

たとえば、要素がシャドウルートをホストしている場合、#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 ツリーをホスト)をネストすることや、<shadow> を使用して別の要素を継承する要素を作成することが挙げられます。

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

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

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

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

querySelector() を使用する

.shadowRoot が DOM トラバーサル用に Shadow ツリーを開くのと同じように、コンビネータはセレクタ トラバーサル用に Shadow ツリーを開きます。ネストされた 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 {
    ...
}

カスタム疑似要素の使用

WebKitFirefox はどちらも、ネイティブ ブラウザ要素の内部スタイルを設定するための疑似要素を定義しています。input[type=range] が良い例です。::-webkit-slider-thumb をターゲットとして、スライダーのつまみ <span style="color:blue">blue</span> のスタイルを設定できます。

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 プロパティを使用して Shadow 境界での動作を制御できます。新しいコンポーネントを作成する際の新たなスタート地点と考えてください。

resetStyleInheritance

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

次のデモは、resetStyleInheritance の変更によって Shadow ツリーがどのように影響を受けるかを示しています。

<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 で継承されるプロパティが不明な場合は、こちらの便利なリストを確認するか、[要素] パネルの [継承されたプロパティを表示] チェックボックスをオンにします。

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

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

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

::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 変数のプレースホルダを含めることで、コンテンツをさらにカスタマイズするための便利なスタイル設定フックをサードパーティに提供できます。まとめると ウェブ作成者はコンテンツの表示方法を 完全に制御できます