LoginSignup
97
77

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

Last updated at Posted at 2023-11-06

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

01.png

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

以下は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との併用に伴い、新たな基準『スコープ近接度』が導入されます。
この優先度は特異性と出現順の間になります。

02.png

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

このルールは、コンポーネントのネストに複数のバリエーションがある場合に便利です。
以下は@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乱れ撃ちの現状も多少は落ち着いてくれるでしょう。
あとはランダムクラス名フレームワークを早急に絶滅させろ。

97
77
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
97
77