CSSの大きな問題点のひとつとして、スタイルが影響する範囲を指定することができませんでした。
そのため一か所だけ書き替えたと思ったら全然関係ないところが崩れたりして、その欠陥をどうにかすべくBEMやらScoped CSSやらStyled Componentsやら解決策が乱立してどうにもならなくなりました。
とりあえずStyled Componentsとかの乱数スタイルシートはユーザスタイルシート適用が困難なのでさっさと滅びろ。
さて先日リリースされたGoogle Chrome 118でCSSが@scopeに対応しました。
なんと、素のCSSで適用範囲を制限できるようになります。
<div class="out">
<span>ここはfooの外</span>
<div class="foo">
<span>ここはfooの中、barの外</span>
<div class="bar">
<span>ここはbarの中</span>
</div>
</div>
</div>
<style>
/* 適用範囲の上限を.foo、下限を.barとする */
@scope (.foo) to (.bar) {
span {
color:red;
}
}
</style>
class=foo
の中であり、かつclass=bar
の外である部分だけが、このCSSの検索範囲となります。
上限だけでなく、両端を決められるのがたいへん便利ですね。
また特異性(後述)に影響がないのがありがたいところです。
以下はChrome公式ブログ、Limit the reach of your selectors with the CSS @scope at-ruleの紹介です。
Limit the reach of your selectors with the CSS @scope at-rule
DOMのサブツリー内の要素だけを選択できる@scope
の使い方について解説します。
The delicate art of writing CSS selectors
セレクタを書く際、2つの相反する要素の狭間で悩むことになるかもしれません。
・選択する要素をできるだけ具体的に明示したい。
・選択する要素をできるだけDOMと疎結合させ、オーバーライドしやすくしたい。
たとえば『cardコンポーネントのコンテンツ領域にある英雄の画像』を選択したい場合、これはかなり限定的な要素になりますが、しかし.card > .content > img.hero
のようなセレクタは書きたくないでしょう。
・子のセレクタは特異性が(0, 3, 1)
とかなり高くなるためオーバーライドしづらい。
・DOM構造と結合度が高いので、マークアップを変更するとCSSも変更になる可能性が高い。
かといってセレクタにimg
とだけ書くと無関係な要素まで選択されてしまいます。
このバランスをとるのは、相当に難しいことでした。
長年の間に、いくつもの解決策や回避策が編み出されました。
・BEMでは、各要素にcard__img card__img--hero
のようなクラスを与えることで、結合度を抑えつつ特異性も低く保ちます。
・Scoped CSSやStyled ComponentsといったJavaScriptベースのソリューションでは、セレクタを全てsc-596d7e0e-4
のようなランダム値に書き替えることで、想定外の要素が選択されないようにします。
・一部のライブラリではセレクタ自体を廃止し、マークアップ側にスタイルのトリガーを記述します。
CSS自体が、特殊性の高いセレクタや結合度の高いセレクタを書くことなく、選択できる要素を具体的に指定する方法を提供してくれることができたらどうでしょう。
そこで登場するのが@scope
です。
これは、DOMのサブツリー内の要素だけを選択する方法を提供します。
Introducing @scope
@scope
は、セレクタの範囲を制限することができます。
まず、対象とするサブツリーの上限となるスコープルートを設定します。
スコープルートに含まれるスタイルルール(スコープ付きスタイルルールと呼びます)は、そのスコープの中でのみ有効となります。
@scope (.card) {
img {
border-color: green;
}
}
スコープ付きスタイルルールimg
は、.card
要素の内部にある<img>
タグだけが選択されることになります。
See the Pen CSS `@scope` demo: scoping root by web.dev (@web-dot-dev) on CodePen.
コンテンツエリア.card__content
の<img>
を選択されたくない場合は<img>
をより詳細に記述することになりますが、もう一つの方法として@scope
に下限を指定することができます。
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
このスコープ付きスタイルルールは、.card
要素の内側であり、かつ.card__content
の外側にある要素にのみ適用されます。
上限と下限を持つこの形式のスコープは、ドーナツスコープと呼ばれます。
See the Pen CSS `@scope` demo: scoping root + scoping limit by web.dev (@web-dot-dev) on CodePen.
The :scope selector
デフォルトでは、全てのスコープ付きスタイルルールは、スコープルートからの相対パスとなります。
スコープルート自体をターゲットとする場合は:scope
疑似クラスが利用できます。
@scope (.card) {
:scope {
/* .card全体にマッチ */
}
img {
/* .card内の<img>にマッチ */
}
}
実際は、スコープ付きスタイルルールのセレクタには全て:scope
が暗黙的に付与されます。
必要であれば明示することもでき、またCSS Nestingの&
も使用可能です。
@scope (.card) {
img {
/* .card内の<img>にマッチ */
}
:scope img {
/* ↑と同じ */
}
& img {
/* これも同じ */
}
}
スコープリミットに:scope
疑似クラスを使い、スコープルートとの関係を記述することもできます。
/* .media-object直下の.contentまでが範囲 */
@scope (.media-object) to (:scope > .content) { ... }
さらにスコープ外の要素を参照することもできます。
/* .sidebarの中にある場合のみ下限が設定される */
@scope (.media-object) to (.sidebar :scope .content) { ... }
スコープの外に出ていくことはできません。
:scope + p
のような記述は、スコープ内に無い要素を選ぼうとするため無効です。
@scope and specificity
@scope
セレクタは、特異性に影響を与えません。
下の例では、img
セレクタの特異性は(0,0,1)
のままです。
@scope (#sidebar) {
img { /* 特異性は (0,0,1) */
…
}
}
:scope
疑似クラスの特異性は、普通の疑似クラスと同じ(0,1,0)
です。
@scope (#sidebar) {
:scope img { /* 特異性は (0,1,0) + (0,0,1) = (0,1,1) */
…
}
}
&
は:is()
セレクタにdesugaringされるので、以下の例は最終的に:is(#sidebar, .card)
と同じになります。
@scope (#sidebar, .card) {
& img { /* `:is(#sidebar, .card) img`と同じ */
…
}
}
&
の特異性は:is()
の特異性に従って計算されます。
今回の例では(1,0,0)
となり、img
の特異性(0,0,1)
と足し合わせて最終的な特異性は(1,0,1)
です。
@scope (#sidebar, .card) {
& img { /* 特異性は (1,0,0) + (0,0,1) = (1,0,1) */
…
}
}
The difference between :scope and & inside @scope
:scope
と&
のもうひとつの違いとして、:scope
はマッチしたスコープルート自体を表しますが、&
はスコープルートとマッチするために使われたセレクタを表します。
@scope (.card) {
& & { /* .card .cardとマッチする */
}
:root :root { /* 何ともマッチしない。訳注:これたぶん:scopeの間違い */
…
}
}
Prelude-less scope
CSSを<style>
でインライン記述する場合、@scope
のスコープルートを省略すると、<style>
を囲む親要素がスコープルートになります。
<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>
この例では、<style>
の親要素であるdiv.card__header
だけがスコープルートになります。
See the Pen CSS `@scope` demo: prelude-less @scope by web.dev (@web-dot-dev) on CodePen.
@scope in the cascade
CSS Cascadeとの併用に伴い、新たな基準『スコープ近接度』が導入されます。
この優先度は特異性と出現順の間になります。
異なるスコープルートを持つ複数のスタイルルールが重なった場合、スコープルートからスタイルルールまでに挟まる要素数が最も少ないスタイルルールが優先されます。
このルールは、コンポーネントのネストに複数のバリエーションがある場合に便利です。
以下は@scope
を使っていない例です。
<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>
3番目のリンクは、div.light
の直下にあるにもかかわらず黒文字ではなく白文字になります。
これはCSSの定義順の基準によるものであり、.dark a
が最後に宣言されたので.light a
より優先されてしまうからです。
See the Pen CSS `@scope` demo: proximity (1/2) by web.dev (@web-dot-dev) on CodePen.
この問題は@scope
を使うことで解決されます。
@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}
@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}
スコープ付きセレクタa
はいずれも同じ特異性を持つため、スコープ近接度の対象になります。
スコープ近接度は、スコープルートからの近さによってセレクタの重み付けを行います。
3番目のa
タグは、スコープルート.light
までは1ホップであり、スコープルート.dark
までは2ホップとなります。
従って、スコープルート.light
のセレクタが勝利します。
See the Pen CSS `@scope` demo: proximity (2/2) by web.dev (@web-dot-dev) on CodePen.
Closing note: Selector isolation, not style isolation
重要な注意点として、@scope
はあくまでセレクタの範囲を制限するものであって、スタイルの分離を提供するものではないとうことです。
子プロパティに継承されるプロパティは、@scope
の下限を超えて継承されます。
たとえばcolor
プロパティなどが該当します。
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
このプロパティは、ドーナツスコープの中にも継承されます。
See the Pen CSS `@scope` demo: selector isolation, not style isolation by web.dev (@web-dot-dev) on CodePen.
この例では、.card__content
とその子要素は.card
から色を継承するため、みんなピンク色になります。
対応状況
Chrome・Edge
最初に記載したとおり、Chrome118で実装されました。
エンジンが同じEdgeも118で対応しています。
Firefox
今のところ目立った動きはないように見えます。
きちんと反応はしているので、いずれ対応されるとは思いますが、いつになるかはわかりません。
Safari
こちらもサポートすると立場を明らかにした以外の進捗は特に見当たりません。
感想
現在はまだChromeしか使用できない方言ということになりますが、いつものChrome独自規格とは異なり他ブラウザも賛同の立場であることから、そのうち実装されると思います。
CSSのネストに引き続き、ようやくまともに構造的なCSSを書けるようになってきましたね。
これが普及すれば、!important
乱れ撃ちの現状も多少は落ち着いてくれるでしょう。
あとはランダムクラス名フレームワークを早急に絶滅させろ。