Shadow DOM 201

CSS とスタイル設定

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

はじめに

実際のところ、スタイル設定されていないマークアップには魅力がありません。幸い、Web コンポーネントの優秀な開発者はこれを予見し、私たちを困らせることなく対応してくれました。CSS スコープ モジュールでは、シャドー ツリー内のコンテンツのスタイル設定に使用できる多くのオプションが定義されています。

スタイルのカプセル化

Shadow DOM のコア機能の 1 つはシャドウ境界です。多くの優れたプロパティがありますが、その中でも特に優れているのは、スタイルのカプセル化を無料で提供できることです。言い換えると、

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

注意すべき点の 1 つは、親ページのルールは要素で定義された :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>

.different クラスの要素の子孫である場合に、:host-context(.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 権限の Vorpal ソードを持っているようなものです。これにより、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 と /deep/ の使用

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

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

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

カスタム疑似要素を使用する

WebKitFirefox の両方で、ネイティブ ブラウザ要素の内部部分のスタイル設定用の疑似要素が定義されています。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 で継承されるプロパティがわからない場合は、こちらの便利なリストを参照するか、[要素] パネルで [継承を表示] チェックボックスをオンまたはオフにします。

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

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