CSS @scopeの基礎知識と使い方を解説、セレクタの適用範囲を設定できる
Post on:2023年11月9日
CSSのセレクタで深い階層の要素を記述するとき、たとえば.card > .content > img.hero
のように長いセレクタになってしまうことがあります。
Chrome 118からサポートされたCSSの@scope
を使用すると、セレクタの適用範囲を設定できるので、簡単になります。上記のセレクタは、.card
内のimg
と記述できます。CSSの@scope
の基礎知識と使い方を紹介します。
Limit the reach of your selectors with the CSS @scope at-rule
by Bramus!
下記は各ポイントを意訳したものです。
※元サイト様のライセンスに基づいて翻訳しています。基づいてというのは、貢献部分に関して同ライセンスも含みます。
- CSSのセレクタを記述するきめ細やかな技術
- CSSの@scopeとは
- :scope疑似クラスとは
- @scopeと詳細度
- :scopeと@scope内の&の違い
- プレリュードなしのスコープ
- カスケードにおける@scope
- 終わりに: @scopeはスタイルの分離ではなく、セレクタの分離
CSSのセレクタを記述するきめ細やかな技術
CSSのセレクタを記述するとき、2つの世界の狭間で悩むことがあるかもしれません。1つは選択する要素についてかなり具体的にしたい、もう1つはオーバーライドしやすくしDOM構造と密接に結びつかないようにしたい、です。
たとえば、「カードコンポーネントのコンテンツエリアにあるヒーロー画像」を選択したい場合、これはかなり具体的な要素ですが、.card > .content > img.hero
のようなセレクタを記述したくはないでしょう。
- このセレクタは
(0,3,1)
と非常に高い詳細度を持っているため、コードが肥大化したときにオーバーライドするのが難しくなります。 - 直接の子に依存しており、DOM構造と密接に結びついています。マークアップが変更された場合、CSSのセレクタも変更する必要があります。
しかし、img
とだけ記述するのも避けたいところです。img
だけにしてしまうと、ページに使用しているすべての画像要素が選択されてしまいます。
このような懸念事項がある中で何を採用するか適切なバランスを見つけることは、かなり難しいです。一部のデベロッパーはこのような状況で役立つ解決策や回避策を考え出してきました。たとえば、
- BEMなどのメソッドでは、その要素に
.card__img
,.card__img--hero
というクラスを与えて、選択するものを具体的にできるようにしながら、詳細度を低く保つようにします。 - Scoped CSSやStyled ComponentsのようなJavaScriptベースのソリューションでは、ランダムに生成される文字列(
sc-596d7e0e-4
など)をセレクタにすることで、すべてのセレクタを書き換え、ページの他の要素を選択しないようにします。 - 一部のライブラリではセレクタが完全に廃止され、マークアップ自体にスタイルのトリガーを直接記述します。
もしこれらのどれもが必要ないとしたらどうですか?
CSSが選択する要素をかなり具体的に指定できる方法を提供してくれたらどうでしょう。しかも、詳細度の高いセレクタやDOMと密接に結びついたセレクタを記述する必要はありません。
そこで登場したのが、@scope
です。DOMのサブツリー内の要素だけを選択する方法を提供します。
CSSの@scopeとは
CSSの@scope
を使用すると、セレクタの範囲を設定できます。範囲を設定するには、対象とするサブツリーの上限を決めるスコープルート〔scoping root〕を設定します。スコープルートを設定すると、その中に含まれるスタイル(スコープ付きスタイルルール〔scoped style rules〕と呼ばれます)はDOMの限られたサブツリーからのみ選択できるようになります。
たとえば、.card
コンポーネント内の<img>
要素だけをターゲットにしたい場合は、スコープルートに@scope (.card) {...}
を設定します。
1 2 3 4 5 |
@scope (.card) { img { border-color: green; } } |
スコープ付きスタイルルールimg {...}
は、一致する.card
要素のスコープ内にある<img>
要素のみにスタイルを適用します。
実際の動作は、デモページでご覧ください。
グレーで表示されているエリアは@scope
ルールの範囲内ではありません。スコープ付きスタイルルールは、明るいエリアの要素のみと一致します。
See the Pen
CSS @scope
demo: scoping root by coliss (@coliss)
on CodePen.
カードのコンテンツエリア(.card__content
)内の<img>
要素が選択されないようにするには、img
セレクタをより具体的にします。@scope
ルールは範囲の下限を設定することもできます。
1 2 3 4 5 |
@scope (.card) to (.card__content) { img { border-color: green; } } |
このスコープ付きスタイルルールは、ツリーの.card
要素と.card__content
要素の間に配置された<img>
要素のみを対象とします。このように上限と下限があるスコープは、ドーナツスコープ〔donut scope〕と呼ばれます。
実際の動作は、デモページでご覧ください。
グレーで表示されているエリアは@scope
ルールの範囲内ではありません。スコープ付きスタイルルールは、明るいエリアの要素のみと一致します。
See the Pen
CSS @scope
demo: scoping root + scoping limit by coliss (@coliss)
on CodePen.
:scope疑似クラスとは
デフォルトでは、すべてのスコープ付きスタイルルールは、スコープルートからの相対パスになります。スコープルート要素自体をターゲットにすることも可能で、:scope
セレクタを使用します。
1 2 3 4 5 6 7 8 |
@scope (.card) { :scope { /* 一致する.card自体を選択 */ } img { /* .cardの子要素であるimg要素を選択 */ } } |
スコープ付きスタイルルール内のセレクタは、暗黙的に:scope
が先頭に追加されています。必要であれば自分で:scope
を先頭に追加することで、明示的に示すことができます。あるいは、CSSでネストを使用して&
セレクタを先頭に追加することもできます。
CSSのネストについては、CSSのネストがついにブラウザで使用できるようになった! 基礎知識、便利な使い方を詳しく解説をご覧ください。
1 2 3 4 5 6 7 8 9 10 11 |
@scope (.card) { img { /* .cardの子要素であるimg要素を選択 */ } :scope img { /* これも.cardの子要素であるimg要素を選択 */ } & img { /* これも.cardの子要素であるimg要素を選択 */ } } |
スコープの制限は、:scope
疑似クラスを使用してスコープルートとの特定関係を要求できます。
1 2 |
/* .contentは、:scopeの直接の子である場合にのみ制限 */ @scope (.media-object) to (:scope > .content) { ... } |
また、スコープルートの外側の要素を参照することもできます。
1 2 |
/* .contentは、:scopeが.sidebar内にある場合にのみ制限 */ @scope (.media-object) to (.sidebar :scope .content) { ... } |
注意点として、スコープ付きスタイルルールはサブツリーをエスケープできません。:scope + p
のような選択はスコープ内にない要素を選択するため無効です。
@scopeと詳細度
@scope
で使用するセレクタの詳細度は、含まれるセレクタの詳細度に影響を受けません。下記のCSSでは、img
セレクタの詳細度は(0,0,1)
のままです。
1 2 3 4 5 |
@scope (#sidebar) { img { /* 詳細度 = (0,0,1) */ … } } |
:scope
の詳細度は通常の疑似クラスと同じ、つまり(0,1,0)
です。
1 2 3 4 5 |
@scope (#sidebar) { :scope img { /* 詳細度 = (0,1,0) + (0,0,1) = (0,1,1) */ … } } |
また、下記のCSSでは&
がスコープルートに使用されるセレクタに書き換えられ、:is()
セレクタ内にラップされます。最終的にブラウザは、マッチングを行うセレクタとして:is(#sidebar, .card) img
を使用します。このプロセスは糖衣構文(Wikipedia)として知られています。
1 2 3 4 5 |
@scope (#sidebar, .card) { & img { /* `:is(#sidebar, .card) img`への糖衣構文 */ … } } |
&
は:is()
を使用して糖衣されるため、&
の詳細度は:is()
の詳細度ルールに従って計算されます。つまり、&
の詳細度はそのもっとも具体的な引数の詳細度になります。
このCSSの場合、:is(#sidebar, .card)
の詳細度はもっとも具体的な引数は#sidebar
であるため(1,0,0)
になります。これをimg
の詳細度(0,0,1)
と組み合わせると、複合セレクタ全体の詳細度は(1,0,1)
になります。
1 2 3 4 5 |
@scope (#sidebar, .card) { & img { /* 詳細度 = (1,0,0) + (0,0,1) = (1,0,1) */ … } } |
:scopeと@scope内の&の違い
詳細度の計算方法の違いに加えて、:scope
と&
のもう1つの違いは:scope
が一致したスコープルートを表すのに対して、&
はスコープルートの一致に使用にされるセレクタを表すことです。
そのため、&
を複数回使用できます。スコープルートの中にスコープルートと一致させることはできないので、一度しか使用できない:scope
とは対照的です。
1 2 3 4 5 6 7 |
@scope (.card) { & & { /* 一致したルート.card内の`.card`を選択 */ } :root :root { /* ❌ これは動作しません */ … } } |
プレリュードなしのスコープ
<style>
要素でインラインスタイルを記述する場合、スコープルートを指定しないことで、<style>
要素を囲む親要素にスタイルルールをスコープすることができます。これは@scope
のプレリュードを省略することでできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<div class="card"> <div class="card__header"> <style> @scope { img { border-color: green; } } </style> <h1>Card Title</h1> <img src="…" height="32" class="hero"> </div> <div class="card__content"> <p><img src="…" height="32"></p> </div> </div> |
上記のコードでは、div
が<style>
要素の親要素であるため、スコープ付きルールはcard__header
というクラス名を持つdiv
内の要素のみを対象とします。
実際の動作は、デモページでご覧ください。
グレーで表示されているエリアは@scope
ルールの範囲内ではありません。スコープ付きスタイルルールは、明るいエリアの要素のみと一致します。
See the Pen
CSS @scope
demo: prelude-less @scope by coliss (@coliss)
on CodePen.
カスケードにおける@scope
CSSカスケードの内部では、@scope
によって「スコープ近接性〔Scoping Proximity〕」という新しいステップが追加されます。このステップは「詳細度〔Specificity〕」の後、「出現順〔Order od Appearance〕」の前です。
異なるスコープルートを持つスタイルルールに現れる宣言を比較する場合、スコープルートとスコープ付きスタイルルールのサブジェクト間の世代または兄弟要素のホップ数がもっとも少ない宣言が優先されます。
この新しいステップ「スコープ近接性」は、コンポーネントの複数のバリエーションを入れ子にする場合に便利です。このCSSでは、まだ@scope
を使用していません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<style> .light { background: #ccc; } .dark { background: #333; } .light a { color: black; } .dark a { color: white; } </style> <div class="light"> <p><a href="#">What color am I?</a></p> <div class="dark"> <p><a href="#">What about me?</a></p> <div class="light"> <p><a href="#">Am I the same as the first?</a></p> </div> </div> </div> |
このコードを表示すると、.light
が適用されたdiv
の子要素であるにもかかわらず、3番目のリンクはブラックではなくホワイトになります。これはカスケードが勝者を決めるために使用する出現順によるものです。.dark a
が最後に宣言されているので、.light a
のルールは勝者となります。
実際の動作は、デモページでご覧ください。
このデモでは@scope
を使用していないため、3番目の<a>
の色が間違っていることに注目してください。
See the Pen
CSS @scope
demo: proximity (1/2) by coliss (@coliss)
on CodePen.
「スコープ近接性」を使用すると、この問題は解決されます。
1 2 3 4 5 6 7 8 9 |
@scope (.light) { :scope { background: #ccc; } a { color: black;} } @scope (.dark) { :scope { background: #333; } a { color: white; } } |
スコープ付きa
セレクタはどちらも同じ詳細度のため、「スコープ近接性」が作動します。スコープルートへの近さで両方のセレクタを重み付けします。3番目のa
要素の場合、.light
のスコープルートまでは1ホップしかありませんが、.dark
までは2ホップです。したがって、.light
のa
セレクタが優先されます。
実際の動作は、デモページでご覧ください。
@scope
のおかげで、3番目の<a>
に正しい色が適用されています。
See the Pen
CSS @scope
demo: proximity (2/2) by coliss (@coliss)
on CodePen.
終わりに: @scopeはスタイルの分離ではなく、セレクタの分離
最後に重要な注意点として、@scope
はセレクタの範囲を制限するものであり、スタイルの分離を提供するものではありません。子プロパティに継承されるプロパティは、@scope
の下限を超えても継承されます。
たとえば、color
プロパティを見てましょう。ドーナツスコープ内で宣言した場合でも、その色はドーナツの穴の中の子に継承されます。
1 2 3 4 5 |
@scope (.card) to (.card__content) { :scope { color: hotpink; } } |
実際の動作は、デモページでご覧ください。
グレーで表示されているエリアは@scope
ルールの範囲内ではありません。スコープ付きスタイルルールは、明るいエリアの要素のみと一致します。継承は妨げられません。
See the Pen
CSS @scope
demo: selector isolation, not style isolation by coliss (@coliss)
on CodePen.
このデモでは、.card__content
要素とその子要素は.card
から値を継承しているため、hotpink
の色になっています。
sponsors