CSSの新常識@scope入門 HTMLとCSSだけでスコープ化して保守地獄を抜ける

Web技術

CSSは自由だ。自由すぎて事故る。コーポレートの改修で、たった1行の追記が別ページのレイアウトを壊す。あの瞬間の血の気の引き方、覚えていますか。

原因はだいたい同じです。グローバルに効くセレクタ、長生きしすぎたユーティリティ、名前の衝突。BEMで逃げても、命名が増えていく。設計に勝った気になって、結局は運用に負ける。あるある。

そこで登場するのが @scope。HTMLとCSSだけで「このコンポーネントの中だけに効かせる」を実現する仕組みです。Shadow DOMほど重くない。CSS Modulesほどビルドに寄らない。ちょうど良い地点。

ただし、万能薬の顔をしてクセはある。使いどころを間違えると逆に読みにくい。なので、現場で手が止まらない形で、空気感ごとまとめます。

  1. @scopeって何をしてくれるのか
    1. スコープルートとスコープリミット
  2. なぜ今までのグローバルCSSをやめたくなるのか
  3. 基本構文 まずはこれだけで動ける
    1. CSSで書く 独立ブロック
    2. HTMLで書く その場スコープ
  4. カスケードのクセ スコープは強い
  5. :scopeの使いどころ @scopeとセットで強くなる
  6. 現場の使いどころ こういう時に刺さる
    1. コンポーネント単位のCSSを増やしていきたい時
    2. 既存のグローバルCSSを崩さずに改善したい時
    3. 入れ子コンポーネントの衝突を止めたい時
  7. やり方 実務で困らない導入手順
    1. 1 まずはベースをグローバルで定義する
    2. 2 コンポーネントCSSだけ@scopeで囲う
    3. 3 既存の強いセレクタを一気に消さない
    4. 4 フォールバックを用意する
  8. メリット 実務で効く気持ち良さ
    1. セレクタの意図が読み取りやすい
    2. 衝突を設計で防げる
    3. 詳細度レースから降りやすい
    4. コンポーネント設計に寄り添う
  9. デメリット ここで詰まる人が多い
    1. カスケードの勝ち方が直感とズレることがある
    2. HTML構造に依存しやすい
    3. チーム内のルールがないと読み味がバラつく
  10. よくあるハマりどころと回避の小技
    1. スコープルートを要素セレクタにしてしまう
    2. 入れ子で破綻する
    3. @scopeの中で全称セレクタを多用する
  11. @scopeと相性が良いCSS機能たち
    1. cascade layersで土台を分ける
    2. container queriesでサイズ依存の分岐を局所化
    3. :where()で詳細度を薄くする
  12. 実務サンプル カードとFAQを@scopeで守る
  13. 読者向けのおまけ 現場で効く運用ルール案
  14. @scopeが刺さる人向け ついでに覚えると得する話

@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だけで成立させる。だから気に入っています。偏見込み。

(Visited 1 times, 1 visits today)
タイトルとURLをコピーしました