ICS MEDIAで新しく掲載された以下の記事を読みました。
https://ics.media/entry/250520/
大前提として、私は ICS MEDIA の記事を愛読していますし、池田さんをはじめとして ICS で働くエンジニアの方々とはリアルで親交もあり、尊敬しています。
上述した「@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 p
で 0.1.1
となります。
これだけ見れば詳細度バトルを放棄しやすいメリットを感じますが、IE時代のCSS設計とは違い、現代では囲ったセレクタの詳細度を0にする:where()
という便利擬似クラスが存在するため、大したメリットには感じません。
:where(.section) {
p {
color: green; /* 詳細度 0.0.1 */
}
}
.section {
:where(p) {
color: green; /* 詳細度 0.1.0 */
}
}
加えて、スコープリミットが存在しない場合は、独立した子コンポーネントの同一セレクタへのスタイルの適用を防ぐことはできません。
例えば .ComponentA
の p
要素はユニークな 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つから選択するのが良いと考えます。
data-scope='ComponentA'
のようにclassではなくカスタムデータで定義する- コンポーネントルートに
.scope
や任意のカスタムデータを持たせてそれをリミットとする .c-Component
のようにプレフィックスを持たせる
<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設計になるので拒否したくなる人は多そう。
コンポーネントルートにスコープリミットの目印となる任意の属性を設ける方法です。
<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
を指定するための強制力が必要になりそうです。
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
を使ったところで「クラス名を複雑にしなくてすむ」ことにはならないと思います。
スコープルートに関してはユニークな class名
じゃないとコンフリクトは避けられないですし、スコープ内のセレクタに関しても CSS Modules のようにユニークなサフィックスが付与されるわけではないので .title
のような汎用的な名前だと外部CSSの影響とバッティングする可能性もあります。
また、スコープリミットの設計も入念に行わないと保守性が高まるどころかより落とす可能性もありますし、不十分だと機能しなくなってスタイルの衝突を防げなくなります。
つまり、BEMが無くなるのではなく、新しく @scope
に適した命名規則を考える必要があるのは覚えといた方がいいでしょう。そして、BEMに満足している現場では @scope
に移行しないほうが幸せになるかもしれません。
あと、フレームワークの Scoped CSS が使える、もしくは Tailwind CSS を利用できる現場なら @scope
を使用するメリットはほぼ無いのでそれらを優先して使ったほうがいいと思います。