【CSS】スタイルが適用される範囲を限定する@scopeが非常に便利で有能

CSSの大きな問題点のひとつとして、スタイルが影響する範囲を指定することができませんでした。
そのため一か所だけ書き替えたと思ったら全然関係ないところが崩れたりして、その欠陥をどうにかすべく[BEM](https://getbem.com/)やら[Scoped CSS](https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html)やら[Styled Components](https://styled-components.com/)やら解決策が乱立してどうにもならなくなりました。
とりあえずStyled Componentsとかの乱数スタイルシートはユーザスタイルシート適用が困難なのでさっさと滅びろ。


さて先日リリースされた[Google Chrome 118](https://developer.chrome.com/blog/new-in-chrome-118/)でCSSが[@scopeに対応しました](https://caniuse.com/?search=%40scope)。
なんと、素のCSSで適用範囲を制限できるようになります。

```html
<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の検索範囲となります。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/9c202645-4fe1-3176-7374-fd6572f3a685.png)

上限だけでなく、両端を決められるのがたいへん便利ですね。
また特異性(後述)に影響がないのがありがたいところです。

以下はChrome公式ブログ、[Limit the reach of your selectors with the CSS @scope at-rule](https://developer.chrome.com/articles/at-scope/)の紹介です。

# 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`のようなセレクタは書きたくないでしょう。

・子のセレクタは[特異性](https://web.dev/learn/css/specificity)が`(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`は、セレクタの範囲を制限することができます。
まず、対象とするサブツリーの上限となるスコープルートを設定します。
スコープルートに含まれるスタイルルール(スコープ付きスタイルルールと呼びます)は、そのスコープの中でのみ有効となります。

```css
@scope (.card) {
    img {
        border-color: green;
    }
}
```

スコープ付きスタイルルール`img`は、`.card`要素の内部にある`<img>`タグだけが選択されることになります。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="YzdOydZ" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/YzdOydZ">
  CSS `@scope` demo: scoping root</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

コンテンツエリア`.card__content`の`<img>`を選択されたくない場合は`<img>`をより詳細に記述することになりますが、もう一つの方法として`@scope`に下限を指定することができます。

```css
@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}
```

このスコープ付きスタイルルールは、`.card`要素の内側であり、かつ`.card__content`の外側にある要素にのみ適用されます。
上限と下限を持つこの形式のスコープは、ドーナツスコープと呼ばれます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="wvREKRj" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/wvREKRj">
  CSS `@scope` demo: scoping root + scoping limit</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


## The :scope selector


デフォルトでは、全てのスコープ付きスタイルルールは、スコープルートからの相対パスとなります。
スコープルート自体をターゲットとする場合は`:scope`疑似クラスが利用できます。

```css
@scope (.card) {
    :scope {
        /* .card全体にマッチ */
    }
    img {
        /* .card内の<img>にマッチ */
    }
}
```

実際は、スコープ付きスタイルルールのセレクタには全て`:scope`が暗黙的に付与されます。
必要であれば明示することもでき、また[CSS Nesting](https://developer.chrome.com/articles/css-nesting/)の`&`も使用可能です。

```css
@scope (.card) {
    img {
        /* .card内の<img>にマッチ */
    }
    :scope img {
        /* ↑と同じ */
    }
    & img {
        /* これも同じ */
    }
}
```

スコープリミットに`:scope`疑似クラスを使い、スコープルートとの関係を記述することもできます。

```css
/* .media-object直下の.contentまでが範囲 */
@scope (.media-object) to (:scope > .content) { ... }
```

さらにスコープ外の要素を参照することもできます。

```css
/* .sidebarの中にある場合のみ下限が設定される */
@scope (.media-object) to (.sidebar :scope .content) { ... }
```

スコープの外に出ていくことはできません。
`:scope + p`のような記述は、スコープ内に無い要素を選ぼうとするため無効です。


## @scope and specificity

`@scope`セレクタは、特異性に影響を与えません。
下の例では、`img`セレクタの特異性は`(0,0,1)`のままです。

```css
@scope (#sidebar) {
    img { /* 特異性は (0,0,1) */
        …
    }
}
```

`:scope`疑似クラスの特異性は、普通の疑似クラスと同じ`(0,1,0)`です。

```css
@scope (#sidebar) {
    :scope img { /* 特異性は (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}
```

`&`は`:is()`セレクタに[desugaring](https://en.wikipedia.org/wiki/Syntactic_sugar)されるので、以下の例は最終的に`:is(#sidebar, .card)`と同じになります。

```css
@scope (#sidebar, .card) {
    & img { /* `:is(#sidebar, .card) img`と同じ */
        …
    }
}
```

`&`の特異性は`:is()`の[特異性](https://brm.us/css-is#specificity)に従って計算されます。
今回の例では`(1,0,0)`となり、`img`の特異性`(0,0,1)`と足し合わせて最終的な特異性は`(1,0,1)`です。

```css
@scope (#sidebar, .card) {
    & img { /* 特異性は (1,0,0) + (0,0,1) = (1,0,1) */
        …
    }
}
```

## The difference between :scope and & inside @scope

`:scope`と`&`のもうひとつの違いとして、`:scope`はマッチしたスコープルート自体を表しますが、`&`はスコープルートとマッチするために使われたセレクタを表します。

```css
@scope (.card) {
  & & { /* .card .cardとマッチする */
  }
  :root :root { /* 何ともマッチしない。訳注：これたぶん:scopeの間違い */
    …
  }
}
```


## Prelude-less scope

CSSを`<style>`でインライン記述する場合、`@scope`のスコープルートを省略すると、`<style>`を囲む親要素がスコープルートになります。

```html
<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`だけがスコープルートになります。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="JjwaYwG" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/JjwaYwG">
  CSS `@scope` demo: prelude-less @scope</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


## @scope in the cascade

[CSS Cascade](https://web.dev/learn/css/the-cascade)との併用に伴い、新たな基準『スコープ近接度』が導入されます。
この優先度は特異性と出現順の間になります。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/cfb635a3-58aa-0c36-3522-9543365ad9a5.png)


異なるスコープルートを持つ複数のスタイルルールが重なった場合、スコープルートからスタイルルールまでに挟まる要素数が最も少ないスタイルルールが優先されます。

このルールは、コンポーネントのネストに複数のバリエーションがある場合に便利です。
以下は`@scope`を使っていない例です。

```html
<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`より優先されてしまうからです。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="eYbLpQw" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/eYbLpQw">
  CSS `@scope` demo: proximity (1/2)</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

この問題は`@scope`を使うことで解決されます。

```css
@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}
```

スコープ付きセレクタ`a`はいずれも同じ特異性を持つため、スコープ近接度の対象になります。
スコープ近接度は、スコープルートからの近さによってセレクタの重み付けを行います。
3番目の`a`タグは、スコープルート`.light`までは1ホップであり、スコープルート`.dark`までは2ホップとなります。
従って、スコープルート`.light`のセレクタが勝利します。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="MWZqazx" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/MWZqazx">
  CSS `@scope` demo: proximity (2/2)</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


## Closing note: Selector isolation, not style isolation

重要な注意点として、`@scope`はあくまでセレクタの範囲を制限するものであって、スタイルの分離を提供するものではないとうことです。
子プロパティに継承されるプロパティは、`@scope`の下限を超えて継承されます。
たとえば`color`プロパティなどが該当します。

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

このプロパティは、ドーナツスコープの中にも継承されます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="BavOoGY" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/BavOoGY">
  CSS `@scope` demo: selector isolation, not style isolation</a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

この例では、`.card__content`とその子要素は`.card`から色を継承するため、みんなピンク色になります。


# 対応状況

## Chrome・Edge

最初に記載したとおり、Chrome118で実装されました。
エンジンが同じEdgeも118で対応しています。


## Firefox

今のところ[目立った動きはないように見えます](https://bugzilla.mozilla.org/show_bug.cgi?id=1830512)。
[きちんと反応はしている](https://bugzilla.mozilla.org/show_bug.cgi?id=1860511)ので、いずれ対応されるとは思いますが、いつになるかはわかりません。

## Safari

こちらも[サポートすると立場を明らかにした](https://github.com/WebKit/standards-positions/issues/13)以外の進捗は特に見当たりません。


# 感想

現在はまだChromeしか使用できない方言ということになりますが、[いつもの](https://qiita.com/rana_kualu/items/93759dfb098e1a3478a1)[Chrome](https://qiita.com/rana_kualu/items/2ca7dad356c5caf34383)[独自規格](https://qiita.com/rana_kualu/items/e470f0303e6b3a429c09)とは異なり他ブラウザも賛同の立場であることから、そのうち実装されると思います。

[CSSのネスト](https://qiita.com/rana_kualu/items/e814f3e13aac48d53aaa)に引き続き、ようやくまともに構造的なCSSを書けるようになってきましたね。

これが普及すれば、`!important`乱れ撃ちの現状も多少は落ち着いてくれるでしょう。
あとはランダムクラス名フレームワークを早急に絶滅させろ。