@scope を使用して、DOM の限定されたサブツリー内の要素のみを選択する方法を学びます。
CSS セレクタの記述の繊細な芸術
セレクタの記述時には、2 つの世界に引き裂かれることがあります。一方で、選択する要素は具体的に指定する必要があります。一方で、セレクタはオーバーライドしやすく、DOM 構造に密結合しないようにする必要があります。
たとえば、「カード コンポーネントのコンテンツ領域にあるヒーロー画像」を選択する場合(これはかなり具体的な要素選択です)は、.card > .content > img.hero
のようなセレクタを記述する必要はありません。
- このセレクタの特異性は
(0,3,1)
とかなり高く、コードの増加に伴いオーバーライドしにくくなります。 - 直接子コンビネータに依存するため、DOM 構造に密接に関連付けられます。マークアップを変更する場合は、CSS も変更する必要があります。
ただし、その要素のセレクタとして img
のみを記述すると、ページ上のすべての画像要素が選択されるため、そのようにはしません。
適切なバランスを見つけるのは、多くの場合、かなりの課題となります。長年にわたり、このような状況を解決するためのソリューションや回避策をデベロッパーが開発してきました。次に例を示します。
- BEM などの方法論では、その要素に
card__img card__img--hero
というクラスを指定することで、選択内容を具体的にしながら、特定度を低く抑えるようにしています。 - スコープ CSS や スタイルコンポーネントなどの JavaScript ベースのソリューションは、ランダムに生成された文字列(
sc-596d7e0e-4
など)をセレクタに追加することですべてのセレクタを書き換え、ページの反対側の要素をターゲットに設定しないようにします。 - 一部のライブラリでは、セレクタを完全に廃止し、スタイル設定トリガーをマークアップ自体に直接配置する必要があります。
では、これらの機能のいずれも必要ない場合はどうすればよいでしょうか。特定度の高いセレクタや DOM に密接に関連付けられたセレクタを記述しなくても、選択する要素をかなり具体的に指定できる方法が CSS にあればどうでしょう。そこで @scope
が役立ちます。DOM のサブツリー内の要素のみを選択する方法を提供します。
@scope の導入
@scope
を使用すると、セレクタの範囲を制限できます。これを行うには、スコープルートを設定します。スコープルートにより、ターゲットとするサブツリーの上限が決まります。スコープ設定ルートを設定すると、そのスコープ設定ルートに含まれるスタイルルール(スコープ設定スタイルルール)は、その限定された DOM サブツリーからのみ選択できます。
たとえば、.card
コンポーネント内の <img>
要素のみをターゲットにするには、.card
を @scope
アットルールのスコーピング ルートとして設定します。
@scope (.card) {
img {
border-color: green;
}
}
スコープ設定されたスタイルルール img { … }
では、一致した .card
要素のスコープ内にある <img>
要素のみを効果的に選択できます。
カードのコンテンツ領域(.card__content
)内の <img>
要素が選択されないようにするには、img
セレクタをより具体的にします。別の方法として、@scope
アットルールが下限を決定するスコープの制限も受け入れるという事実を利用することもできます。
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
このスコープ設定されたスタイルルールは、祖先ツリー内の .card
要素と .card__content
要素の間に配置された <img>
要素のみを対象とします。上限と下限のあるこのタイプのスコープは、ドーナツ スコープと呼ばれます。
:scope
セレクタ
デフォルトでは、すべてのスコープ設定されたスタイルルールはスコープ設定ルートに対して相対的です。スコープ設定のルート要素自体をターゲットにすることもできます。これには、:scope
セレクタを使用します。
@scope (.card) {
:scope {
/* Selects the matched .card itself */
}
img {
/* Selects img elements that are a child of .card */
}
}
スコープ設定されたスタイルルール内のセレクタには、暗黙的に :scope
が先頭に追加されます。必要に応じて、:scope
を先頭に追加して明示的に指定することもできます。または、CSS のネストから &
セレクタを先頭に追加することもできます。
@scope (.card) {
img {
/* Selects img elements that are a child of .card */
}
:scope img {
/* Also selects img elements that are a child of .card */
}
& img {
/* Also selects img elements that are a child of .card */
}
}
スコープ制限では、:scope
疑似クラスを使用して、スコープルートに特定の関係を要求できます。
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
スコープ制限では、:scope
を使用してスコープルートの外部にある要素を参照することもできます。次に例を示します。
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
スコープ設定されたスタイルルール自体はサブツリーをエスケープできません。:scope + p
などの選択は無効です。これは、スコープ外の要素を選択しようとするためです。
@scope
と特定性
@scope
のプリルード内で使用するセレクタは、含まれるセレクタの特定性に影響しません。次の例では、img
セレクタの特定度は引き続き (0,0,1)
です。
@scope (#sidebar) {
img { /* Specificity = (0,0,1) */
…
}
}
:scope
の固有性は、通常の疑似クラス((0,1,0)
)の固有性と同じです。
@scope (#sidebar) {
:scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
…
}
}
次の例では、内部で &
が、スコープ ルートに使用されるセレクタに書き換えられ、:is()
セレクタでラップされます。最終的に、ブラウザは :is(#sidebar, .card) img
をセレクタとして使用して照合を行います。このプロセスは「脱糖化」と呼ばれます。
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
…
}
}
&
は :is()
を使用して脱糖されるため、&
の特殊性は :is()
の特殊性ルールに従って計算されます。&
の特殊性は、最も具体的な引数の特殊性です。
この例に適用すると、:is(#sidebar, .card)
の特異性は、最も具体的な引数(#sidebar
)の特異性であるため、(1,0,0)
になります。これを img
の固有性((0,0,1)
)と組み合わせると、複合セレクタ全体の固有性は (1,0,1)
になります。
@scope (#sidebar, .card) {
& img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
…
}
}
@scope
内の :scope
と &
の違い
特異性の計算方法の違いに加えて、:scope
と &
のもう 1 つの違いは、:scope
が一致したスコープルートを表し、&
がスコープルートの一致に使用されるセレクタを表す点です。
そのため、&
を複数回使用できます。これは、スコープルート内のスコープルートを照合できないため、1 回しか使用できない :scope
とは対照的です。
@scope (.card) {
& & { /* Selects a `.card` in the matched root .card */
}
:scope :scope { /* ❌ Does not work */
…
}
}
プレリュードのないスコープ
<style>
要素でインライン スタイルを記述する場合、スコープルートを指定しないことで、スタイルルールを <style>
要素を囲む親要素にスコープできます。これを行うには、@scope
のプリルードを省略します。
<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
CSS カスケード内では、@scope
によって新しい基準(スコープの近接性)も追加されます。ステップは、特定度の後に、表示順序の前に指定します。
仕様に従い、
異なるスコープルートを持つスタイルルールに表示される宣言を比較する場合、スコープルートからスコープされたスタイルルールの対象までの世代要素または兄弟要素のホップ数が最も少ない宣言が優先されます。
この新しいステップは、コンポーネントの複数のバリエーションをネストする場合に便利です。次の例では、まだ @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 番目のリンクは、クラス .light
が適用された div
の子であるにもかかわらず、black
ではなく white
になります。これは、カスケードが勝者を決定するために使用する表示順序の条件が原因です。.dark a
が最後に宣言されたため、.light a
ルールで優先されます。
スコープの近接性基準により、この問題は解決しました。
@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
はセレクタの範囲を制限するものであり、スタイルの分離は提供しません。子に継承されるプロパティは、@scope
の下限を超えて引き続き継承されます。そのようなプロパティの 1 つが color
です。ドーナツ スコープ内で宣言した場合でも、color
はドーナツの穴内の子に継承されます。
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
上記の例では、.card__content
要素とその子は .card
から値を継承するため、hotpink
の色になっています。
(表紙写真: Unsplash の rustam burkhanov)