🦸‍♂️

Cascade Layerは汚れ切ったCSSの救世主になれるのか?

2023/01/16に公開

Cascade Layerとは

最近(といっても10か月ほど前ですが)全ブラウザで対応されたCSSの新機能
CSSのカスケードを実装者側でより自由にコントロールできるようになります。
https://developer.mozilla.org/ja/docs/Web/CSS/@layer

CSSのカスケードについてのおさらい

CSSではHTMLの要素にスタイルを適用する際に、詳細度を基準として適用されるルールが決定されます。

セレクタの詳細度の決まり方

セレクタに登場するID/CLASS/TYPEの数をそれぞれ合計したものがそのルールの詳細度です

#id{}       /*1-0-0*/
.class{}      /*0-1-0*/
div{}             /*0-0-1*/
#id .class div{}  /*1-1-1(上のセレクタの各行ごとの和になっている)*/

1つの要素に対して2つ以上の箇所でスタイルの宣言がされた場合、上記の詳細度の列を左から比較していって、数値の高いほうが適用されます。

こちらの例では、下のセレクタのほうが長く、場するセレクタも多いので一見下のほうが優先度が高そうに見えますが、詳細度を数値化すると上が2-0-0下が1-1-3となっているため、ID列の数値の大きい上のセレクタのルールが適用されています。

ID列の数値が同じ場合、次のCLASS列の値を比較し、大きいほうが適用されます。
そのため、この例では上が1-2-0下が1-1-1となっているため上のルールが適用されます。
CLASS列まで同じ場合はさらにその次のTYPE列の数値で比較します。

ID/CLASS/TYPEすべてが同じ数値の場合は後から宣言されたルールが適用されます。

より詳細な内容を知りたい場合は下記を参照してください。
https://developer.mozilla.org/ja/docs/Web/CSS/Specificity

汚れ切ったCSSとは

汚れ切ったCSSとは以下のような特徴を持ったCSSのことを指しています。

  • 10年以上前に書かれた自前のリセットCSSを使っている(いわゆる秘伝のたれ)
  • class名の命名規則がない
  • 設定系のスタイル指定に詳細度の高いセレクタが使われている
  • 設定系のスタイルが広範囲にかかる形で指定されている
  • セレクタ指定のルールがない
  • 1ページに複数のスタイルシートを読み込んでいるが、どのファイルに何についてのスタイルが書かれているかが明確ではない

このような環境だと、新しいコードを追加するときに以前書いたCSSのどの部分が干渉してしまうか不明瞭になり、意図しないスタイルが適用されてしまいます。
また、それを上書きするためにより詳細度の高いセレクタを指定して上書きさせることを繰り返すことで、簡単に負の連鎖に陥ってしまいます。
1人で作業するならまだマシで、複数人で作業するときにほかの人がいじったcssの影響を受けてしまい、その分の修正作業が発生してしまうことは予想に難くありません。

全部のコードを書き直せれば良いんですが、大規模なサイトになればなるほど影響の調査が必要だったり、下手に直そうとすると変なところが崩れたり、そもそもリファクタリングのための予算が出なかったりでなかなかそんなことはできないんですよね。世知辛いね。

Cascade Layerの使い方

基本的な文法

layer.css
@layer base{
	#main div{
		margin: 0;
	}
}
@layer component{
	div{
		margin: 1rem;
	}
}

@layerルールを使用してレイヤーを宣言することができます。
複数のレイヤーが宣言された場合、あとから宣言されたレイヤーが優先されるようになります。
優先度が高いレイヤーで定義されたスタイルは、優先度が低いレイヤーで定義されたスタイルより詳細度が低い場合でも優先的に適用されます。
この例では#main div1-0-1div0-0-1とIDをセレクタに含んでいるほうが詳細度が高くなっていますが、優先度の高いcomponentレイヤーで宣言されたmargin:1remがスタイルとして適用されます。

:::

複数回の宣言

layer.css
@layer base{
	#main div{
		margin: 0;
	}
}
@layer component{
	div{
		margin: 1rem;
	}
}
+ @layer base{
+ 	#main div{
+ 		margin: 0;
+ 	}
+ }

やっぱりmargin:0を適用したくなり、このように下にルールを追加しました。
しかし、複数回同じレイヤーが呼び出された場合は最初に定義された際のレイヤーの優先度が保持されるため、divにはmargin:1remが適用されます

一括宣言

layer.css
+ @layer base, component;
@layer base{
	#main div{
		margin: 0;
	}
}
@layer component{
	div{
		margin: 1rem;
	}
}
@layer base{
	#main div{
		margin:0;
	}
}

レイヤーはカンマ区切りで複数のものを一括で宣言することができます。
その際、優先度は左から右に行くにつれて高くなります。
各レイヤーの優先度の見通しが良くなるため、Cascade Layerを使用する場合は積極的に採用するべき構文だと思います。

ネストしたレイヤー

レイヤーはネストして宣言することができます。

layer.css
@layer component{
	.btn{
		display: block;
		padding: 1rem;
		font-size: black;
	}
	@layer modifier{
		.btn--active{
			background-color: #01a;
		}
	}
}
@layer component.modifier{
	.btn--active{
		background-color: #00f;
	}
}

ネストされているレイヤーにルールを追加する場合は、親子関係にあるレイヤー名を.でつないで宣言します。
また、(直感的ではありませんが)親レイヤーのほうが子レイヤーよりも優先されるようになっています。
このpenはcomponentcomponent.specialli:last-of-typeにそれぞれlist-styleを設定したときの表示がどうなるかを示したものです。

無名レイヤー

layer.css
@layer{
	div{
		background: revert;
	}
}

必要に応じて名前を持たないレイヤーを宣言することができます。
名前がないので当然ですが後からルールを割り当てることはできません。

レイヤーとインポート

サードパーティ製のライブラリや全ページで共通のCSSを読み込む場合、通常@importルールを使用してCSSファイル内で読み込むことができます。
その時、layer()関数を使用することで、任意のレイヤーにインポートしたcssファイルのルールを割り当てることができます。

@layer base, common, component, special;
@import url("/common/reset.css") layer(base);
@import url("/common/common.css") layer(common);

Cascade Layerを使用するメリット

今までのCSSが抱える問題点

詳細度は各ファイル間で共有され比較される

例えば

/(documentRoot)
┝common
│  ┝ reset.css //ブラウザのデフォルトのスタイルを打ち消すcss
│  └ common.css // header/footerなどサイト全体で共通の部分のcss
┝ contact
│  ┝ index.html
│  ┝ css
│  │  ┝ common.css // サイト内の/contact配下で共通の部分のcss (レイアウトやボタンなどの共通のコンポーネントなど)
│  │  └ style.css // /contact/index.html のみで使用するファイル
│  ┝ confirm
│  │  ┝ index.html
│  │  └ style.css // /contact/confirm/index.html のみで使用するファイル
~

のようなディレクトリ・ファイル構成のプロジェクトを考えた場合、/contact/confirm/index.htmlで読み込むべきcssファイルは

  • /common/reset.css
  • /common/common.css
  • /contact/css/common.css
  • /contact/confirm/style.css

の4点になります。
この時、しっかりとしたcss設計のもとにコードが書かれていれば特に問題は起きにくいと思いますが、どこかで誰かがidセレクタを使用した時点でこれらのCSSは冒頭で述べた汚れ切ったCSSになるのにそう時間はかからないでしょう。
例えば

/common/common.css
#main p{ /*#mainは<main id="main">とします*/
 margin: .5rem 1rem;
}

とでもしようものなら、それに勝つためにそれ以降p要素へパディングを指定する場合は必ずセレクタにidを含めないといけないことになるからです。

Cascade Layerを使用するとどうなるか

上で紹介したCascade Layerの機能のうち、レイヤーとインポートはそのようなケースで大きな効力を発揮できるでしょう

/contact/confirm/style.css
@layer base, common, component;
@import url("/common/reset.css") layer(base);
@import url("/common/common.css") layer(common);
@import url("/contact/css/common.css") layer(component);
/*
======================================
ここから下にページ固有のスタイルを記入
======================================
*/

とすると、他のファイルでのセレクタの詳細度を気にすることなくページ内での作業に集中することができます。
もちろん同じファイル内では詳細度が高いほうが適用されるので、BEMを使用するなどしてなるべく詳細度は低い状態でスタイルを指定できるよう保つ工夫は引き続き必要ですが。。。

Cascade Layerで解決できないこと

!important

優先度が低いレイヤーでも!importantを使用するとスタイルが適用されてしまいます。
そもそも使うな

inline style

いくらレイヤーを駆使して詳細度をコントロールしてもインラインのstyleには(!importantを使わない限り)普通に負けます。
JSから操作する以外の目的で使うな

class名の衝突

Cascade Layerを使用することで解決できるのはあくまで一つの要素に対する複数のセレクタの詳細度の問題のみです。
複数の要素に指定された同じclass名(それも意図的なものではなくうっかりかぶってしまったもの)についてはこれまでと同じように命名規則などCSS設計での対策が必要になります

まとめ

Cascade Layerはこれまで難しかったことが簡単にできるといった類の機能ではないと思います。
ただ、この機能がある喜びは汚れ切ったCSSで疲弊したことがある人にはかなりうれしい機能ではないでしょうか。
また、これからのCSS設計には間違いなく重要になってくる概念だと思うので、機会があれば積極的に取り入れてみるのも面白そうです。

また、これ以外にも

  • SCSSのようにセレクタを入れ子構造にできるCSS Nesting Module
  • 特定の子要素を持っている要素にマッチする:has()セレクタ
  • 画面幅ではなく親要素の幅でスタイルを指定できるコンテナクエリー
  • 特定の範囲にセレクタの影響範囲を閉じ込めることができる@scopeルール

など、最近実装されたあるいは現在仕様策定中のCSSの新機能たちはかなりパワフルなので、それを受けてCSSの設計の常識も全く変わるのでしょうか?注目です。

Discussion