@scope を使用して、DOM の限定されたサブツリー内にある要素のみを選択する方法を学習します。
対応ブラウザ
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
CSS セレクタの緻密な記述方法
セレクタを記述すると、2 つの世界に行き詰まるかもしれません。一方では、選択する要素をかなり具体的にする必要があります。その一方で、セレクタはオーバーライドしやすく、DOM 構造に密接に結びついていない状態にする必要があります。
たとえば、カード コンポーネントのコンテンツ領域にあるヒーロー画像(特定の要素を選択する)を選択する場合、.card > .content > img.hero
のようなセレクタは記述しないほうがよいでしょう。
- このセレクタは
(0,3,1)
という非常に高い限定性を備えているため、コードが大きくなった場合にオーバーライドするのは困難です。 - 直接の子コンビネータを使用することで、DOM 構造と密接に結びついています。マークアップが変更された場合は、CSS も変更する必要があります。
ただし、その要素のセレクタとして img
だけを記述するのはおすすめしません。ページ全体のすべての画像要素が選択されるためです。
多くの場合、ここで適切なバランスを見つけるのは至難の業です。これまで何年もの間、一部のデベロッパーは、このような状況で役立つ解決策や回避策を考案しています。例:
- BEM などの手法では、その要素に
card__img card__img--hero
のクラスを割り当てて、特異性を低く抑えつつ、選択する要素を指定できるようにします。 - スコープ CSS やスタイル付きコンポーネントなど、JavaScript ベースのソリューションでは、ランダムに生成された文字列(
sc-596d7e0e-4
など)をセレクタに追加して、ページの反対側にある要素がターゲットとならないように、すべてのセレクタを書き換えます。 - 一部のライブラリではセレクタが完全に廃止されており、スタイル設定のトリガーをマークアップ自体に直接配置する必要があります。
しかし、そのいずれも必要ないとしたらどうでしょうか。CSS に、選択する要素をかなり具体的に指定する方法があるとしたらどうでしょうか。高い限定性のセレクタや、DOM に密接に結びついたセレクタを作成する必要がないとしたらどうでしょうか。そこで役立つのが @scope
です。DOM のサブツリー内の要素のみを選択する方法を提供します。
@scope のご紹介
@scope
を使用すると、セレクタのリーチを制限できます。これを行うには、ターゲットにするサブツリーの上限を決定するスコープ対象ルートを設定します。スコープ対象ルートセットを使用すると、含まれているスタイルルール(スコープ設定スタイルルール)は、DOM の限られたサブツリーからのみ選択できるようになります。
たとえば、.card
コンポーネントの <img>
要素のみをターゲットとするには、@scope
アットルールのスコープ ルートとして .card
を設定します。
@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 Nesting の &
セレクタを先頭に追加することもできます。
@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
と &
のもう一つの違いは、: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
の下限を超えても継承されます。そのようなプロパティの一つが color
です。ドーナツのスコープ内にあることを宣言すると、color
は引き続きドーナツの穴内の子に継承されます。
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
上記の例では、.card__content
要素とその子は .card
の値を継承しているため、hotpink
色になっています。
(カバー写真作成者: rustam burkhanov Unsplash より)