CSS @scope at-rule でセレクタのリーチを制限する

@scope を使用して、DOM の限定されたサブツリー内の要素のみを選択する方法を学びます。

Bramus
Bramus

対応ブラウザ

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 旗の裏側。
  • Safari: 17.4。

ソース

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 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__headerdiv 内の要素のみを対象とします。

カスケードの @scope

CSS カスケード内では、@scope によって新しい基準(スコープの近接性)も追加されます。ステップは、特定度の後に、表示順序の前に指定します。

CSS カスケードの可視化。

仕様に従い、

異なるスコープルートを持つスタイルルールに表示される宣言を比較する場合、スコープルートからスコープされたスタイルルールの対象までの世代要素または兄弟要素のホップ数が最も少ない宣言が優先されます。

この新しいステップは、コンポーネントの複数のバリエーションをネストする場合に便利です。次の例では、まだ @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 つです。.lighta セレクタが優先されます。

結びの注記: スタイル分離ではなくセレクタ分離

重要な注意事項として、@scope はセレクタの範囲を制限するものであり、スタイルの分離は提供しません。子に継承されるプロパティは、@scope の下限を超えて引き続き継承されます。そのようなプロパティの 1 つが color です。ドーナツのスコープ内にあることを宣言すると、color は引き続きドーナツの穴内の子に継承されます。

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

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

(表紙写真: Unsplashrustam burkhanov