Skip to content

Instantly share code, notes, and snippets.

@tak-dcxi
Last active May 23, 2025 15:55
@scope入門」という記事を読んで改めて @scope の設計方法を考える

「@scope入門」という記事を読んで改めて @scope の設計方法を考える

ICS MEDIAで新しく掲載された以下の記事を読みました。

https://ics.media/entry/250520/

大前提として、私は ICS MEDIA の記事を愛読していますし、池田さんをはじめとして ICS で働くエンジニアの方々とはリアルで親交もあり、尊敬しています。

上述した「@scope入門」という記事に関しても @scope の基礎的な使い方が纏まっており、黎明期ゆえに文献の少ない @scope にフィーチャーした参考記事としては質の高い記事であることは間違いありません。

それを踏まえた上で、こちらの記事には同意できない点が複数見受けられ、同時にこの先 @scope を利用することになった場合、メンバーに記事内で触れられている記述を真似してほしくないと感じたので、私なりの考えを記します。

@scope を利用するメリット

「@scope入門」では @scope を利用するメリットについて以下のように紹介されています。

  • クラス名を複雑にしなくてすむ
  • スタイルの衝突を防ぎやすくなる
  • 保守性が高まる

これらに関しては正しく利用できるのなら至極真っ当であり、反論の余地は無いでしょう。

CSS Modules のような Scoped CSS が利用できる環境や Tailwind CSS を利用している環境では@scopeを使用するメリットはありませんが、BEMのようなオールドファッションな命名規則を強いられている環境ではメリットを享受できそうです。

@scope は BEM における block-element の関係を、ネスト記法は element--modifier の関係を表現するのに優れているため、MindBEMdingの思想をそのまま流用できる良いアプローチだと感じます。

ただし、「BEMから脱出できるぞ!やったー!」という感情を優先して考えなしに @scope を利用すると思わぬ落とし穴にハマる可能性があります。

前提:スコープリミットは必須である

@scope は記事内でも触れられているようにスコープリミットは省略可能となっています。ですが、運用する上ではスコープリミットは必須と考えてください。

例えばサンプルコードでは以下のような記述がされています。

@scope (.section) {
  p {
    color: green;
  }
}

が、お気づきの方も多いでしょうが、これは以下のように記述するのと大差ありません。

.section {
  p {
    color: green;
  }
}

違いとしては @scope () 内で定義されたセレクタは詳細度に影響しない(記事内では触れられていない仕様)ため、@scope した p 要素の詳細度は 0.0.1、ネストした p 要素の詳細度は .section p0.1.1 となります。

これだけ見れば詳細度バトルを放棄しやすいメリットを感じますが、IE時代のCSS設計とは違い、現代では囲ったセレクタの詳細度を0にする:where()という便利擬似クラスが存在するため、大したメリットには感じません。

:where(.section) {
  p {
    color: green; /* 詳細度 0.0.1 */
  }
}

.section {
  :where(p) {
    color: green; /* 詳細度 0.1.0 */
  }
}

加えて、スコープリミットが存在しない場合は、独立した子コンポーネントの同一セレクタへのスタイルの適用を防ぐことはできません。

例えば .ComponentAp 要素はユニークな background-color を持たせつつ、.ComponentB のそれには背景色の適用を望まないといったケースでは、スコープリミットを持たせないと .ComponentA のエレメントのスタイルが適用されてしまいます。

<div class="ComponentA">
  <p>...</p>
  <div class="ComponentB">
    <p>...</p>
  </div>
  <p>...</p>
</div>
@scope (.ComponentA) {
  p {
    background-color: #eee;
    padding: 1em;
  }
}

以上の理由からスコープリミットを設定しない @scope の利用にメリットは少なく、それであれば似たような形で書けて互換性の高いネスト記法で十分だと感じます。

スコープリミットに具体性を持たせてはいけない

ここからが本題です。

記事では「スコープの範囲を細かく制御したい場合」の例として以下のようなサンプルコードが挙げられています。

<div class="section">
  <p>適用されません</p>

  <div class="section_footer">
    <p>適用されます</p>

    <div class="section_footer_textarea">
      <p>適用されます</p>

      <div class="section_footer_textarea_inner">
        <p>適用されません</p>
      </div>
    </div>
  </div>
</div>
@scope (.section_footer) to (.section_footer_textarea_inner) {
  p {
    color: green;
  }
}

上記の例では .section_footer_textarea_inner というスコープリミットを設定し、.section_footer からそこまでをスコープ対象としています。

「クラス名を複雑にしなくてすむ」のが利点なのに .section_footer_textarea_inner という命名はOKなのか?とか気になる部分はありますが、本質はそこではないので省略します。

私たちが @scope に望むことはフレームワークの Scoped CSS のようにコンポーネントベースでスコープを設けて、コンポーネントの CSS はそのコンポーネントだけに適用されるという挙動でしょう。

そこで、サンプルコードを以下のように書き直した場合に問題点が出てきます。

<div class="section">
  <p>適用されます</p>

  <div class="section_header">
    <hgroup class="hgroup">
      <h2 class="hgroup_title">タイトル</h2>
      <p class="hgroup_subtitle">サブタイトル(適用されます)</p>
    </hgroup>
  </div>

  <div class="section_footer">
    <p>適用されます</p>

    <div class="section_footer_textarea">
      <p>適用されます</p>

      <div class="section_footer_textarea_inner">
        <p>適用されません</p>
      </div>
    </div>
  </div>
</div>
@scope (.section) to (.section_footer_textarea_inner) {
  p {
    color: green;
  }
}

.section 内に .section_header を追加して、その中に汎用的なコンポーネントである .hgroup を内包します。

このような追加対応を行った場合にスコープリミットは .section_footer_textarea_inner のみなので .hgroup の中の副題にもスタイルが適用されてしまいます。

これを防ぐためには @scope (.section) to (.section_header, .section_footer_textarea_inner) のようにスコープリミットにセレクタを追加する必要性が出てきます。

@scope (.section) to (.section_header, .section_footer_textarea_inner) {
  p {
    color: green;
  }
}

もし、.section_main が出てきたらそれもスコープリミットに追加しないといけません。これでは @scope のメリットして挙げられていた「保守性が高まる」とは真逆に進んでいる印象です。

そもそも、「コンポーネントの CSS はそのコンポーネントだけに適用される」という思想を満たすのならスコープリミットを設けるべきはエレメントではなく内包しているコンポーネントに対してでしょう。

だからと言って、次のような指定をしてしまったらおしまいです。

@scope (.section) to (.hgroup) {
  p {
    color: green;
  }
}

スコープリミットに別コンポーネントの定義を含めた時点でコンポーネントの独立性を放棄してしまい、再利用性のかけらも無くなってしまいます。

つまり、スコープリミットに求められるものは「コンポーネントの CSS はそのコンポーネントだけに適用される」という条件を満たしつつ、汎用性の高いものにする必要があります。

スコープリミットの設計

スコープリミットの指定方法に関しては以下の3つから選択するのが良いと考えます。

  1. data-scope='ComponentA' のようにclassではなくカスタムデータで定義する
  2. コンポーネントルートに .scope や任意のカスタムデータを持たせてそれをリミットとする
  3. .c-Component のようにプレフィックスを持たせる

data-scope='ComponentA' のようにclassではなくカスタムデータで定義する

<div data-scope="ComponentA">
  <p class="_Description">グレー色の背景、1em分のpadding、赤色のテキスト</p>
  <div data-scope="ComponentB">
    <p class="_Description">背景色なし、paddingなし、青色のテキスト</p>
  </div>
</div>
@scope ([data-scope='ComponentA']) to ([data-scope]) {
  & {
    color: oklch(from red calc(l - 0.1) c h);
    border: 2px solid;
    padding: 1em;
  }

  ._Description {
    background-color: #eee;
    padding: 1em;
  }
}

@scope ([data-scope='ComponentB']) to ([data-scope]) {
  & {
    color: oklch(from blue calc(l + 0.1) c h);
    border: 2px solid;
  }
}

これは仕様書にて活用例のアイデアとして紹介されている手法です。

副作用は少ないものの、エキゾチックなCSS設計になるので拒否したくなる人は多そう。

コンポーネントルートに .scope や任意のカスタムデータを持たせてそれをリミットとする

コンポーネントルートにスコープリミットの目印となる任意の属性を設ける方法です。

<div class="scope ComponentA">
  <p class="_Description">グレー色の背景、1em分のpadding、赤色のテキスト</p>
  <div class="scope ComponentB">
    <p class="_Description">背景色なし、paddingなし、青色のテキスト</p>
  </div>
</div>
@scope (.scope.ComponentA) to (.scope) {
  & {
    color: oklch(from red calc(l - 0.1) c h);
    border: 2px solid;
    padding: 1em;
  }

  ._Description {
    background-color: #eee;
    padding: 1em;
  }
}

@scope (.scope.ComponentB) to (.scope) {
  & {
    color: oklch(from blue calc(l + 0.1) c h);
    border: 2px solid;
  }
}

比較的わかりやすい設計だと思いつつ、指定漏れが起きないようにコンポーネントルートに必ず .scope を指定するための強制力が必要になりそうです。

.c-Componentのようにプレフィックスを持たせる

FLOCSS のようにコンポーネントルートには .c-** というプレフィックスを持たせてスコープリミットは [class|="c"] とする方法。

<div class="c-ComponentA">
  <p class="_Description">グレー色の背景、1em分のpadding、赤色のテキスト</p>
  <div class="c-ComponentB">
    <p class="_Description">背景色なし、paddingなし、青色のテキスト</p>
  </div>
</div>
@scope (.c-ComponentA) to ([class|="c"]) {
  & {
    color: oklch(from red calc(l - 0.1) c h);
    border: 2px solid;
    padding: 1em;
  }

  ._Description {
    background-color: #eee;
    padding: 1em;
  }
}

@scope (.c-ComponentB) to ([class|="c"]) {
  & {
    color: oklch(from blue calc(l + 0.1) c h);
    border: 2px solid;
  }
}

現在でも FLOCSS は広く使用されており、プレフィックスをつけることに抵抗感が無い実装者は多く、ルール厳守のやりやすさにも優れています。

ただし、デメリットも多く目立ちます。

  • FLOCSS を使用している方々は Layout, Components, Projects を Components に集約する必要があります。
    • とは言え、何が Layout or Components or Projects で、何が Layout or Components or Projects でないかは微妙なケースが多く、線引きも人によって異なります。また、プレフィックスを見て「これはProjects用のコンポーネントだ!」ということが分かっても大して嬉しいことはないです。重複しないような具体的かつ明確な命名ができるのなら全部 Components に寄せても問題ないと感じます。
  • スコープリミットに [class|="c"] というまどろっこしい指定をする必要があります。スニペット機能を使うなどして効率化したい。
  • class 属性は c- から始まる必要があるので class=".foo .c-BarComponent" のような指定になったら破綻します。

個人的には 2. か 3. を選択したいという気持ちです。

結論:@scope を使うなら @scope 用の新しい CSS 設計を考える必要がある

@scope を使ったところで「クラス名を複雑にしなくてすむ」ことにはならないと思います。

スコープルートに関してはユニークな class名 じゃないとコンフリクトは避けられないですし、スコープ内のセレクタに関しても CSS Modules のようにユニークなサフィックスが付与されるわけではないので .title のような汎用的な名前だと外部CSSの影響とバッティングする可能性もあります。

また、スコープリミットの設計も入念に行わないと保守性が高まるどころかより落とす可能性もありますし、不十分だと機能しなくなってスタイルの衝突を防げなくなります。

つまり、BEMが無くなるのではなく、新しく @scope に適した命名規則を考える必要があるのは覚えといた方がいいでしょう。そして、BEMに満足している現場では @scope に移行しないほうが幸せになるかもしれません。

あと、フレームワークの Scoped CSS が使える、もしくは Tailwind CSS を利用できる現場なら @scope を使用するメリットはほぼ無いのでそれらを優先して使ったほうがいいと思います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment