Shadow DOM 201

CSS とスタイリング

HTML5 Rocks

この記事では Shadow DOM で可能になる、さらに素晴らしいことについて述べていきます。Shadow DOM 101 で述べた知識が前提となりますので、もしまだ読まれていない方はぜひそちらをご覧下さい。

はじめに

スタイルが当てられていないマークアップほど美しくないものはありません。しかし、Web Components に関わってきた先人たち のおかげで、Shadow Tree にスタイルを当てるたくさんの方法が CSS Scoping Module として定義されています。

Chrome であれば、about:flags ページにある "Enable experimental Web Platform features" フラグを有効にすることで、この記事に書かれているすべての機能を試すことができます。

スタイルのカプセル化

Shadow DOM の主要機能のひとつに Shadow Boundary があります。ポイントはたくさんありますが、そのうちのひとつとしてスタイルのカプセル化が挙げられます。別の言い方をすると:

Shadow DOM で定義された CSS スタイルは ShadowRoot 内にスコープされている。これはスタイルがデフォルトでカプセル化されていることを意味する。

下記に例を示します。もしあなたのブラウザが Shadow DOM をサポートしていれば、"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>

Light DOM

このデモの結果から 2 つの事が分かります:

  • このページには他にも h3 タグ が存在していますが、マッチして赤く表示されている h3 セレクターは ShadowRoot にぶら下がっているものだけです。これはデフォルトでスタイルがスコープされていることを表しています。
  • このページで h3 に定義されている他のスタイルルールが、デモのコンテンツ内部に入り込んでいません。これは セレクターが Shadow Boundary を超えられない ためです。

このことから分かるのは、Shadow DOM がスタイルを外部からカプセル化している、ということです。

ホスト要素にスタイルを与える

:host を使うことで、Shadow Tree をホストしている要素を選択し、スタイルを与えることができます:

<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 のユースケースとして、Custom Element を作って、ユーザーの状態 (: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 に、コンテキストに応じてスタイルルールをカプセル化して当てることが可能になります。

Support multiple host types from within one shadow root

:host の他の使い方としては、あなたがテーマライブラリを作りたいとして、同じ Shadow DOM 内から様々なホスト要素をスタイリングしたいケースが挙げられます。

:host(x-foo) { 
  /* ホストが <x-foo> 要素の場合に適用 */
}

:host(x-foo:host) { 
  /* 同上。ホストが <x-foo> 要素の場合に適用 */
}

:host(div) {  {
  /* ホスト要素が <div> の場合に適用 */
}

外側から Shadow DOM 内部をスタイリングする

::shadow 擬似要素および /deep/ コンビネータは、CSS の権限に関して鋭い刃の剣を持つようなものです。Shadow DOM の境界線を突き抜け、Shadow Tree 内のスタイル要素にまで影響を与えることができます。

::shadow 擬似要素

要素が少なくともひとつの Shadow Tree を持っている場合、::shadow 擬似要素は ShadowRoot とマッチします。これにより、要素の Shadow DOM 内のノードにスタイルを当てるセレクターを書くことが可能になります。

例えば、ある要素が ShadowRoot を持っている場合、#host::shadow span {} とすることで、Shadow Tree 内のすべての <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>
Light DOM

(Custom Elements) - <x-tabs> が Shadow DOM の子要素に <x-panel> を持ち、各パネルが h2 ヘッダー内にそれぞれ Shadow Tree をホストしている場合、メインページから各ヘッダーをスタイリングするには:

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

/deep/ コンビネータ

/deep/ コンビネータは ::shadow に似ていますが、よりパワフルです。Shadow Boundary を完全に無視し、Shadow Tree をいくつでも乗り越えてしまいます。単純に言ってしまえば、/deep/ は要素の内部まで入り込み、どんなノードでもターゲットにすることができます。

/deep/ コンビネータは、複数階層にまたがる Shadow DOM が使われることの多い Custom Elements の世界でとりわけ有用です。何層にもネストされた Custom Elements や、<shadow> を使って他の要素から継承される場合が主なユースケースとなります。

(Custom Elements) - <x-tabs> の子要素となるすべての <x-panel> 要素を選択する:

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

- Shadow Tree 内にあるすべての .library-theme クラスを持った要素をスタイリングする:

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

querySelector() との合わせ技

.shadowRoot の代わりにコンビネータを使って Shadow Tree を開くことができます。.querySelector() をチェーンさせず、ひとつのステートメントで記述するなら:

// 楽しくない
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

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

Styling native elements

ネイティブな HTML 要素をスタイリングするのは至難の業です。ほとんどの人が諦めて自分の要素を作ってしまいます。しかしながら、::shadow/deep/ を駆使することで、ウェブプラットフォーム上の Shadow DOM を使ったネイティブ要素は、スタイリングが可能です。<input><video> が良い例でしょう:

video /deep/ input[type="range"] {
  background: hotpink;
}
::shadow 擬似要素と /deep/ コンビネータは、スタイルカプセル化の持つ意義をなくすものでしょうか?Shadow DOM は、デフォルトでは外部からの 意図しない スタイリングを防ぎますが、防弾チョッキではありません。開発者は、何をやっているのかを把握できている限りにおいて、意図的に Shadow Tree 内をスタイリングできるべきです。これにより柔軟性も上がりますし、テーマを作ったり、作った要素の再利用性も向上します。

Creating style hooks

カスタマイゼーションはよいものです。場合によっては、Shadow DOM の境界線に穴を開けて、外部からスタイル可能にしたいこともあるのではないでしょうか?

Using ::shadow and /deep/

/deep/ にはコンポーネント作者が各要素をスタイリング可能にしたり、テーマを当てられるようにすることができる秘められたパワーがあります。

- Shadow Tree を無視して .library-theme を持つすべての要素をスタイリングする:

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

CSS Variables を使う

テーマを可能にする方法に CSS Variables を使ったものが挙げられます。これはひとことで言えば、第三者が埋められる「スタイルのプレースホルダー」です。

Custom Element の作者が Shadow DOM に変数のプレースホルダーを用意した場合を想像してください。ひとつは内部のボタンに用いるフォント、もうひとつは色です:

button {
  color: var(--button-text-color, pink); /* デフォルトの色はピンク */
  font-family: var(--button-font);
}

そして、要素の利用者は好みに応じてその値を定義します。例えばページのテーマに合わせてカッコいい Comic Sans フォントを使うとか:

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

CSS Variables の継承に則って、全てが桃のように、美しくなりました!まとめるとこんな感じ:

<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>
この記事で何度か Custom Elements という言葉を使いました。ひとまず、Shadow DOM がスタイリングと DOM のカプセル化をもって、その構造の基礎を成すということだけ知っておいて下さい。ここでのコンセプトは Custom Elements のスタイリングに関連するものです。

分散ノードのスタイリング

分散ノードとは、挿入ポイント でレンダリングされる要素 (<content> 要素) のことです。<content> 要素は Light DOM (Shadow DOM と対義) からノードを選択し、Shadow DOM 内で予め決められた場所にレンダリングするためのものです。<content> は、論理的には Shadow DOM 内部ではなく、ホスト要素の子要素のままです。挿入ポイントは、レンダリングを司るものでしかありません。

分散ノードは親ドキュメントからのスタイルを保持します。つまり、挿入ポイントでレンダリングされたとしても、その要素に適用された親ドキュメントからのスタイルルールは適用されるということです。繰り返しになりますが、分散ノードは論理的には Light DOM 内にあり、動きません。レンダリングの位置が変更されるだけです。ただし、ノードが Shadow DOM 内に分散されると、Shadow Tree 内で定義されたスタイルが適用されます。

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

Light DOM

I'm not underlined

I'm underlined in Shadow DOM!

"Shadow DOM" と、その下に "Light DOM" が見えるはずです。"Light DOM" が margin などのスタイルを保持したままであることにも注目して下さい。これはページのスタイルがまだ一致しているためです。

ホストドキュメントで定義されたスタイルは、Shadow DOM 内で分散されたとしても、指し示すノードに適用され続けます。適用されたものは、挿入ポイントであっても変わらないのです。

まとめ

Custom Elements の作者には、コンテンツの見た目を制御するオプションがたくさん用意されています。Shadow DOM はこの新しい世界の基礎を形作るものなのです。

Shadow DOM は、スコープされたスタイルのカプセル化と、必要に足るだけ外界の意図を持ち込む手段を提供します。カスタム擬似要素や CSS Variable のプレースホルダーを定義することで、要素の作者は第三者にさらなるカスタマイズの自由を与えることができます。これは言い換えれば、ウェブ製作者がコンテンツの見た目に関して、全権を握っているということです。

このチュートリアルの内容をレビューしてくれた Dominic CooneyDimitri Glazkov に感謝します。

Comments

0