CSS @scopeの基礎知識と使い方を解説、セレクタの適用範囲を設定できる

CSSのセレクタで深い階層の要素を記述するとき、たとえば.card > .content > img.heroのように長いセレクタになってしまうことがあります。

Chrome 118からサポートされたCSSの@scopeを使用すると、セレクタの適用範囲を設定できるので、簡単になります。上記のセレクタは、.card内のimgと記述できます。CSSの@scopeの基礎知識と使い方を紹介します。

CSS @scopeの基礎知識と使い方を解説

Limit the reach of your selectors with the CSS @scope at-rule
by Bramus!

下記は各ポイントを意訳したものです。
※元サイト様のライセンスに基づいて翻訳しています。基づいてというのは、貢献部分に関して同ライセンスも含みます。

CSSのセレクタを記述するきめ細やかな技術

CSSのセレクタを記述するとき、2つの世界の狭間で悩むことがあるかもしれません。1つは選択する要素についてかなり具体的にしたい、もう1つはオーバーライドしやすくしDOM構造と密接に結びつかないようにしたい、です。

たとえば、「カードコンポーネントのコンテンツエリアにあるヒーロー画像」を選択したい場合、これはかなり具体的な要素ですが、.card > .content > img.heroのようなセレクタを記述したくはないでしょう。

  • このセレクタは(0,3,1)と非常に高い詳細度を持っているため、コードが肥大化したときにオーバーライドするのが難しくなります。
  • 直接の子に依存しており、DOM構造と密接に結びついています。マークアップが変更された場合、CSSのセレクタも変更する必要があります。

しかし、imgとだけ記述するのも避けたいところです。imgだけにしてしまうと、ページに使用しているすべての画像要素が選択されてしまいます。

このような懸念事項がある中で何を採用するか適切なバランスを見つけることは、かなり難しいです。一部のデベロッパーはこのような状況で役立つ解決策や回避策を考え出してきました。たとえば、

  • BEMなどのメソッドでは、その要素に.card__img, .card__img--heroというクラスを与えて、選択するものを具体的にできるようにしながら、詳細度を低く保つようにします。
  • Scoped CSSStyled ComponentsのようなJavaScriptベースのソリューションでは、ランダムに生成される文字列(sc-596d7e0e-4など)をセレクタにすることで、すべてのセレクタを書き換え、ページの他の要素を選択しないようにします。
  • 一部のライブラリではセレクタが完全に廃止され、マークアップ自体にスタイルのトリガーを直接記述します。

もしこれらのどれもが必要ないとしたらどうですか?
CSSが選択する要素をかなり具体的に指定できる方法を提供してくれたらどうでしょう。しかも、詳細度の高いセレクタやDOMと密接に結びついたセレクタを記述する必要はありません。

そこで登場したのが、@scopeです。DOMのサブツリー内の要素だけを選択する方法を提供します。

CSSの@scopeとは

CSSの@scopeを使用すると、セレクタの範囲を設定できます。範囲を設定するには、対象とするサブツリーの上限を決めるスコープルート〔scoping root〕を設定します。スコープルートを設定すると、その中に含まれるスタイル(スコープ付きスタイルルール〔scoped style rules〕と呼ばれます)はDOMの限られたサブツリーからのみ選択できるようになります。

たとえば、.cardコンポーネント内の<img>要素だけをターゲットにしたい場合は、スコープルートに@scope (.card) {...}を設定します。

スコープ付きスタイルルールimg {...}は、一致する.card要素のスコープ内にある<img>要素のみにスタイルを適用します。

実際の動作は、デモページでご覧ください。
グレーで表示されているエリアは@scopeルールの範囲内ではありません。スコープ付きスタイルルールは、明るいエリアの要素のみと一致します。

See the Pen
CSS @scope demo: scoping root
by coliss (@coliss)
on CodePen.

カードのコンテンツエリア(.card__content)内の<img>要素が選択されないようにするには、imgセレクタをより具体的にします。@scopeルールは範囲の下限を設定することもできます。

このスコープ付きスタイルルールは、ツリーの.card要素と.card__content要素の間に配置された<img>要素のみを対象とします。このように上限と下限があるスコープは、ドーナツスコープ〔donut scope〕と呼ばれます。

実際の動作は、デモページでご覧ください。
グレーで表示されているエリアは@scopeルールの範囲内ではありません。スコープ付きスタイルルールは、明るいエリアの要素のみと一致します。

See the Pen
CSS @scope demo: scoping root + scoping limit
by coliss (@coliss)
on CodePen.

:scope疑似クラスとは

デフォルトでは、すべてのスコープ付きスタイルルールは、スコープルートからの相対パスになります。スコープルート要素自体をターゲットにすることも可能で、:scopeセレクタを使用します。

スコープ付きスタイルルール内のセレクタは、暗黙的に:scopeが先頭に追加されています。必要であれば自分で:scopeを先頭に追加することで、明示的に示すことができます。あるいは、CSSでネストを使用して&セレクタを先頭に追加することもできます。

CSSのネストについては、CSSのネストがついにブラウザで使用できるようになった! 基礎知識、便利な使い方を詳しく解説をご覧ください。

スコープの制限は、:scope疑似クラスを使用してスコープルートとの特定関係を要求できます。

また、スコープルートの外側の要素を参照することもできます。

注意点として、スコープ付きスタイルルールはサブツリーをエスケープできません。:scope + pのような選択はスコープ内にない要素を選択するため無効です。

@scopeと詳細度

@scopeで使用するセレクタの詳細度は、含まれるセレクタの詳細度に影響を受けません。下記のCSSでは、imgセレクタの詳細度は(0,0,1)のままです。

:scopeの詳細度は通常の疑似クラスと同じ、つまり(0,1,0)です。

また、下記のCSSでは&がスコープルートに使用されるセレクタに書き換えられ、:is()セレクタ内にラップされます。最終的にブラウザは、マッチングを行うセレクタとして:is(#sidebar, .card) imgを使用します。このプロセスは糖衣構文(Wikipedia)として知られています。

&:is()を使用して糖衣されるため、&の詳細度は:is()の詳細度ルールに従って計算されます。つまり、&の詳細度はそのもっとも具体的な引数の詳細度になります。

このCSSの場合、:is(#sidebar, .card)の詳細度はもっとも具体的な引数は#sidebarであるため(1,0,0)になります。これをimgの詳細度(0,0,1)と組み合わせると、複合セレクタ全体の詳細度は(1,0,1)になります。

:scopeと@scope内の&の違い

詳細度の計算方法の違いに加えて、:scope&のもう1つの違いは:scopeが一致したスコープルートを表すのに対して、&はスコープルートの一致に使用にされるセレクタを表すことです。

そのため、&を複数回使用できます。スコープルートの中にスコープルートと一致させることはできないので、一度しか使用できない:scopeとは対照的です。

プレリュードなしのスコープ

<style>要素でインラインスタイルを記述する場合、スコープルートを指定しないことで、<style>要素を囲む親要素にスタイルルールをスコープすることができます。これは@scopeのプレリュードを省略することでできます。

上記のコードでは、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カスケード

異なるスコープルートを持つスタイルルールに現れる宣言を比較する場合、スコープルートとスコープ付きスタイルルールのサブジェクト間の世代または兄弟要素のホップ数がもっとも少ない宣言が優先されます。

この新しいステップ「スコープ近接性」は、コンポーネントの複数のバリエーションを入れ子にする場合に便利です。このCSSでは、まだ@scopeを使用していません。

このコードを表示すると、.lightが適用されたdivの子要素であるにもかかわらず、3番目のリンクはブラックではなくホワイトになります。これはカスケードが勝者を決めるために使用する出現順によるものです。.dark aが最後に宣言されているので、.light aのルールは勝者となります。

実際の動作は、デモページでご覧ください。
このデモでは@scopeを使用していないため、3番目の<a>の色が間違っていることに注目してください。

See the Pen
CSS @scope demo: proximity (1/2)
by coliss (@coliss)
on CodePen.

「スコープ近接性」を使用すると、この問題は解決されます。

スコープ付きaセレクタはどちらも同じ詳細度のため、「スコープ近接性」が作動します。スコープルートへの近さで両方のセレクタを重み付けします。3番目のa要素の場合、.lightのスコープルートまでは1ホップしかありませんが、.darkまでは2ホップです。したがって、.lightaセレクタが優先されます。

実際の動作は、デモページでご覧ください。
@scopeのおかげで、3番目の<a>に正しい色が適用されています。

See the Pen
CSS @scope demo: proximity (2/2)
by coliss (@coliss)
on CodePen.

終わりに: @scopeはスタイルの分離ではなく、セレクタの分離

最後に重要な注意点として、@scopeはセレクタの範囲を制限するものであり、スタイルの分離を提供するものではありません。子プロパティに継承されるプロパティは、@scopeの下限を超えても継承されます。

たとえば、colorプロパティを見てましょう。ドーナツスコープ内で宣言した場合でも、その色はドーナツの穴の中の子に継承されます。

実際の動作は、デモページでご覧ください。
グレーで表示されているエリアは@scopeルールの範囲内ではありません。スコープ付きスタイルルールは、明るいエリアの要素のみと一致します。継承は妨げられません。

See the Pen
CSS @scope demo: selector isolation, not style isolation
by coliss (@coliss)
on CodePen.

このデモでは、.card__content要素とその子要素は.cardから値を継承しているため、hotpinkの色になっています。

sponsors

top of page

©2024 coliss