CSSは自由だ。自由すぎて事故る。コーポレートの改修で、たった1行の追記が別ページのレイアウトを壊す。あの瞬間の血の気の引き方、覚えていますか。
原因はだいたい同じです。グローバルに効くセレクタ、長生きしすぎたユーティリティ、名前の衝突。BEMで逃げても、命名が増えていく。設計に勝った気になって、結局は運用に負ける。あるある。
そこで登場するのが @scope。HTMLとCSSだけで「このコンポーネントの中だけに効かせる」を実現する仕組みです。Shadow DOMほど重くない。CSS Modulesほどビルドに寄らない。ちょうど良い地点。
ただし、万能薬の顔をしてクセはある。使いどころを間違えると逆に読みにくい。なので、現場で手が止まらない形で、空気感ごとまとめます。
@scopeって何をしてくれるのか
一言でいうと、セレクタの射程を切り詰めます。いつもの .card img みたいな指定を「カードの中のimgだけ」に閉じ込められる。見た目は普通のCSS。効き方が違う。
ここでのポイントは「セレクタを短くできる」ことじゃない。短くするのは副産物。狙いは「他所に飛ばない」ことです。飛ばないCSSは、仕事を終えた後も人を眠らせる。
スコープルートとスコープリミット
@scopeには2つの境界が出てきます。上の境界がスコープルート。下の境界がスコープリミット。日本語にすると強そうですが、要は「ここからここまで」です。
@scope (.card) {
img { inline-size: 100%; block-size: auto; }
.title { font-weight: 700; }
}
この場合、.card配下にだけルールが当たります。別の場所にある .title は無傷。安全運転。
そして「ここから先には入っていくな」を指定したい時がある。例えばカードの中に、別コンポーネントのカードが入っている時。入れ子で全部赤くなったら困る。そんな時は to() でリミットを引きます。
@scope (.card) to (.card .card) {
.title { color: #111; }
p { line-height: 1.8; }
}
ざっくり言うと、外側のカードだけに効かせて、内側のカードには降りていかない。境界線を引けるのが気持ち良い。
なぜ今までのグローバルCSSをやめたくなるのか
やめる必要があるのは「グローバル」という性格そのもの。設計が悪いというより、寿命が長いほど破壊力が上がる。
例えば、こういうやつ。
.contents p { margin-block: 1.2em; }
書いた瞬間は正義。数カ月後、別のページで「カード内だけ詰めたい」が発生する。上書きする。セレクタが伸びる。次の人が読む。ため息。負債が育つ。
@scopeは、そもそも「その下でしか存在しないルール」にできる。だから上書き合戦になりにくい。BEMの命名で世界を救う話ではなく、ルールの射程で火事を減らす話です。
基本構文 まずはこれだけで動ける
書き方は2パターンあります。CSSファイルで独立ブロックとして書く方法と、HTMLの <style> を置いた場所を起点にする方法。
CSSで書く 独立ブロック
@scope (.profile) {
.name { font-size: 1.125rem; }
.meta { opacity: .72; }
}
ルートを自分で指定するので、構造の意図が読み取りやすい。チーム開発向き。
HTMLで書く その場スコープ
HTML側に <style> を置くと、その親要素がスコープルートとして扱われる書き方があります。デモやプロトタイプで便利。ただ、運用で多用すると「CSSどこ」になりやすい。ほどほどが吉。
<section class="faq">
<style>
@scope {
h3 { margin-block: 0 0.75rem; }
p { margin-block: 0; }
}
</style>
<h3>よくある質問</h3>
<p>...</p>
</section>
ここでの @scope は前置きがありません。親要素が起点になる。直感的。
カスケードのクセ スコープは強い
@scopeは「効く範囲を狭める」だけじゃなく、カスケードにも影響します。ざっくり言うと、スコープ付きのルールは非スコープのルールより優先されやすい。詳細度で殴り合う前に、土俵が違う感じ。
だから、既存の大きなCSSに @scope を足す時は、いきなり全面移行しない方が楽です。ベースは今まで通り。コンポーネントの局所だけ @scope で守る。段階的にいく。
:scopeの使いどころ @scopeとセットで強くなる
:scopeは「今の起点」を指す擬似クラスです。@scopeブロック内で使うと、スコープルート自身を指定できます。これが地味に効く。
@scope (.card) {
:scope { border: 1px solid #ddd; border-radius: 12px; }
.title { margin-block: 0 0.5rem; }
}
.card と書けば済むのに、と思うかもしれない。だが、ここで :scope を使うと「このブロックはcardの話だよ」が文章として通る。読み手に優しいCSS、ちょっと勝ち。
現場の使いどころ こういう時に刺さる
派手なデモより、地味な改修で効く場面を挙げます。あなたの案件に混ぜ込みやすい順番。
コンポーネント単位のCSSを増やしていきたい時
WordPressでも、静的でも、EJSでも。コンポーネントを積み上げるほど「局所のスタイル」が増えます。@scopeはその局所を明示できる。命名に頼りすぎない。
既存のグローバルCSSを崩さずに改善したい時
全改修は無理。でも直したい。あるある。@scopeは「ここだけ安全」にできるので、移行コストが小さい。段階導入が現実的。
入れ子コンポーネントの衝突を止めたい時
カードの中にカード。アコーディオンの中にカード。タブの中にフォーム。現代のDOMは入れ子だらけ。to()で境界線を引けるのが効いてくる。
やり方 実務で困らない導入手順
机上で正しくても、現場の導入は泥臭い。ここは泥臭くいきます。
1 まずはベースをグローバルで定義する
タイポグラフィ、リセット、レイアウトの骨格。これは今まで通りで良い。@scopeで全部包もうとすると読みづらいし、意図が散る。
2 コンポーネントCSSだけ@scopeで囲う
例えばカード、ボタン、バナー、FAQ、フォーム。影響範囲が想像しやすい塊から包む。
/* card.css */
@scope (.c-card) to (.c-card .c-card) {
:scope { padding: 1rem; border-radius: 12px; }
.c-card__title { font-size: 1.125rem; }
.c-card__text { line-height: 1.9; }
}
ここで「c-」は好み。BEMでも良い。@scopeは命名規則の代わりではない。命名が軽くなるだけ。
3 既存の強いセレクタを一気に消さない
消すと事故る。置き換えは段階でいい。@scope内のルールが想定外に勝つ場合があるから、影響範囲の確認は必要。急に勝つCSS、強い。
4 フォールバックを用意する
@scope非対応ブラウザ向けに、最低限の見た目をグローバルに残す。対応ブラウザでは @scope で整える。これで「見た目が崩壊」は避けられます。
/* fallback */
.c-card img { inline-size: 100%; height: auto; }
/* enhancement */
@scope (.c-card) {
img { inline-size: 100%; block-size: auto; }
}
非対応ブラウザは @scopeブロックを無視するので、ベースだけ当たる。気が楽。
ところで、あなたの案件は「古い端末の比率」が高いですか。社内端末が多いサイトだと、ここを雑にすると後で泣きます。
メリット 実務で効く気持ち良さ
セレクタの意図が読み取りやすい
このブロックは何の話か。@scope (.c-card) だけで伝わる。行間が減る。レビューが速くなる。
衝突を設計で防げる
命名の衝突、構造の衝突。気合で防ぐより、仕組みで防ぐ方が堅い。人間は忘れる。CSSは忘れない。
詳細度レースから降りやすい
詳細度を上げるほど戻れない。:where()で薄める手もあるが、@scopeは「そもそも当たらない」方向。思考が違う。
コンポーネント設計に寄り添う
コンポーネントの境界があるなら、CSSにも境界が欲しい。@scopeはその素直な願いに近い。
デメリット ここで詰まる人が多い
カスケードの勝ち方が直感とズレることがある
「詳細度で勝つ」が身体に染みていると、@scopeの勝ち方は変に感じる。だから導入は小さく始めるのが良い。いきなり全面はやめた方がいい。
HTML構造に依存しやすい
スコープはDOMツリーが前提です。構造変更が多いプロジェクトだと、ルートの取り方を雑にすると壊れやすい。とはいえ、グローバルCSSよりは把握しやすいことも多い。
チーム内のルールがないと読み味がバラつく
どこまでを @scope にするか。ルートはクラスか、要素か。to()は使うか。ここがバラつくと、CSSがまた物語を始める。運用ルールは軽く決めたい。
あなたのチーム、命名ルールはありますか。あるなら、そのまま @scope を足すだけで良い。ないなら、まずは「コンポーネントだけ囲う」から始めるのが現実的です。
よくあるハマりどころと回避の小技
スコープルートを要素セレクタにしてしまう
@scope (section) みたいにすると、影響が大きくなりやすい。作業者が増えるほど危険。基本はクラスが無難。コンポーネントの境界をクラスで握る。
入れ子で破綻する
カードの中のカード、または共通パーツの中に別パーツ。入れ子が見えたら to() を検討。境界線を引くと落ち着きます。
@scopeの中で全称セレクタを多用する
* { ... } は強い。便利だが、読み手に優しくない。やるなら .c-card > * + * みたいに意図が分かる形に寄せたい。縦余白だけ揃えるなら、あなたがよく使うstackパターンが相性良い。
.c-card { display: block; }
@scope (.c-card) {
:scope > * + * { margin-block-start: 1rem; }
h3 + * { margin-block-start: .5rem; }
}
余白が整うと、文章もUIも急に賢く見える。ずるい。
@scopeと相性が良いCSS機能たち
cascade layersで土台を分ける
リセット、ベース、コンポーネント、ユーティリティ。@layerで層を分け、コンポーネント内を @scope で閉じる。二段構え。強い。
container queriesでサイズ依存の分岐を局所化
コンポーネントの幅に応じてレイアウトを変えるなら @container。@scope と組むと「このカードはこのカードの中で完結」感が増す。ページ全体のブレイクポイントに引っ張られにくい。
:where()で詳細度を薄くする
グローバル側の詳細度を薄くし、局所は @scope で守る。上書き合戦が減る。読みやすいCSSへ。
実務サンプル カードとFAQを@scopeで守る
ありがちなUIを2つ。コピペで雰囲気が掴めるようにします。
/* Card */
@scope (.c-card) to (.c-card .c-card) {
:scope {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 12px;
background: #fff;
}
.c-card__title {
font-size: 1.125rem;
margin-block: 0 0.5rem;
}
.c-card__text {
margin: 0;
line-height: 1.9;
overflow-wrap: anywhere;
}
img {
inline-size: 100%;
block-size: auto;
border-radius: 10px;
}
}
/* FAQ */
@scope (.c-faq) {
:scope { padding: 1rem; border-radius: 12px; background: #f7f7f7; }
.c-faq__q { font-weight: 700; margin: 0; }
.c-faq__a { margin: .5rem 0 0; line-height: 1.9; }
}
グローバルに p を触っていない。コンポーネント内で完結。読み手も安心。保守する人も安心。
読者向けのおまけ 現場で効く運用ルール案
- スコープルートは原則クラス。要素セレクタは最終手段
- 入れ子コンポーネントが見えたらto()の検討を忘れない
- ベースはグローバル、コンポーネントは@scope。いきなり全部はやらない
- 既存の強いセレクタは段階的に置き換え。消すより共存から
- レビューでは「この@scopeの境界は妥当か」を最初に見る
@scopeが刺さる人向け ついでに覚えると得する話
@scopeを触ると、CSSの悩みが「命名」から「境界」に寄ります。その流れで相性が良いのが次の領域。
- cascade layersで設計を層に分ける
- container queriesでコンポーネント主導のレスポンシブへ
- アクセシビリティのためにprefers-reduced-motionも一緒に点検する
- 余白の統一はstackパターンでルール化して事故を減らす
CSSは、増やすほど強くなるのではなく、分けるほど強くなる。@scopeは、その分け方をHTMLとCSSだけで成立させる。だから気に入っています。偏見込み。
