こんにちは! サイボウズ フロントエンドエキスパートチームの麦島です。
今回取り上げるCSSの機能はCascade Layersです。
Cascade Layersは、@layer
を用いて宣言します。
Cascade Layersと優先順位
CSSを適用する際、CSSセレクタを用いた詳細度での優先順位のコントロールが広く利用されています[1]。
しかし、新たにCascade Layersを組み合わせた場合には次の順番で適用されます。
- Cascade Layers
- CSSセレクタでの詳細度指定
Cascade LayersがCSSセレクタよりも優先して適用される点が重要なポイントです。これにより、役割や目的に応じたCSS設計の大きな助けとなります。
実際に例を見てみましょう。
Cascade Layersの利用例/役割に応じたCSSファイルの分割
CSSの定義がある程度の規模を超えると、すべてを単一ファイルに定義して管理するのは困難となります。対策として、役割に応じてCSSファイルを分割して管理する手法が広く用いられています。
今回は、CSSを
- 共通CSS
- すべてのページで適用するベースとなるCSSを定義。
- ページ固有CSS
- ページ単位で必要となるCSSを定義。必要に応じて共通CSSを上書きする。
- ユーティリティCSS
- 限定的かつ局所的に装飾するためのユーティリティクラスを提供する。共通CSS/
ページ固有CSSの両方を上書きする。
同じ要素に対してスタイルを適用した場合、ユーティリティCSS → ページ固有CSS → 共通CSSの順に優先されることが期待されます。
この3ファイルに分割したCSSを用いて、次のようなスタイルを作成します。
- 複数の記事
(Article) へのリンクを表示する a
要素の基本的なスタイルは共通CSSで定義する- ページ固有のCSSで、
a
要素のスタイルを一部上書きし、ボーダーで囲う - 先頭の記事へのリンクのみ、ユーティリティCSSを用いて大きく表示する
ベースとなるHTMLは次のような内容です。
<!-- head内 -->
<link href="common.css" rel="stylesheet" />
<link href="page.css" rel="stylesheet" />
<link href="utilities.css" rel="stylesheet" />
<!-- body内 -->
<a href="#">Article 1</a>
<a href="#">Article 2</a>
<a href="#">Article 3</a>
最終的に期待する見た目は次のとおりです。
Cascade Layersを用いない場合の例
まずはCascade Layersを用いない従来の実装例を確認してみましょう。
共通CSSで、すべてのリンクのフォントサイズと余白を定義します。
/* 共通CSS(common.css) */
a {
font-size: 12px;
padding: 8px;
}
ページ固有CSSでは、共通CSSのフォントサイズを上書きして個別のスタイルを適用します。padding
は上書きせず、共通CSSのものを継承します。
/* ページ固有CSS(page.css) */
a {
font-size: 14px;
border: 1px solid gray;
}
ユーティリティCSSでは、フォントサイズと余白を調整するためのユーティリティを提供します。先頭要素のみスタイルを調整するために利用します。
/* ユーティリティCSS (utilities.css) */
.util-text-large {
font-size: 24px !important;
}
.util-padding {
padding: 16px !important;
}
そしてHTMLでユーティリティCSSを適用するためのクラスを付与します。
<!-- body内 -->
<a href="#" class="util-text-large util-padding">Article 1</a>
<a href="#">Article 2</a>
<a href="#">Article 3</a>
HTMLで指定したCSSがそれぞれ読み込まれると、共通CSSに加えてページ固有CSSで一部が上書きされ、かつ先頭要素だけはユーティリティCSSで特別なスタイルが適用されます。期待どおりに表示されるため特に問題ないように思えますが、実際はいくつかの課題を抱えています。
問題点:詳細度を常に意識する必要がある
最も大きい問題が、セレクタの詳細度によって簡単に表示が変わってしまう点です。たとえば、次のように共通CSSにホバー時のスタイルを追加するとどうなるでしょうか。
/* 共通CSS(common.css) */
a:hover {
font-size: 12px;
color: red;
}
この場合、共通CSSのセレクタa:hover
の詳細度がページ固有CSSのセレクタa
の詳細度を上回ります。
/* 共通CSS(common.css) */
/* 詳細度: 0-1-1 */
a:hover {
font-size: 12px;
color: red;
}
/* ページ固有CSS(page.css) */
/* 詳細度: 0-0-1 */
a {
font-size: 14px;
border: 1px solid gray;
}
結果として、
ひとつの回避策として:where()
擬似クラスを利用して共通CSSの詳細度を0とする方法が考えられます。しかし、サードパーティ製ライブラリなどの外部CSSを利用している場合には該当ファイル内容を独自で書き換える必要が生じてしまい、リスクも伴い対応が難しくなります。
他にも、ユーティリティCSSを確実に適用させるため!important
を利用している点も問題となりえるでしょう。共通CSSやページ固有CSSでどのような詳細度が指定されるかは不明であり、確実に上回るためには!important
を付与するか、高い詳細度を持つ必要が生じます。
また、分割したCSS間で詳細度が同じ場合には、暗黙的にlink
要素の順序にも依存しています。link
要素の順番を入れ替えた場合に意図せず見た目が変化する恐れがあります。
Cascade Layersによる役割の分割
では、Cascade Layersがどう問題を解決するか見てみましょう。
Cascade Layersでは@layer
でレイヤーを宣言します。ここではそれぞれcommon
、page
、utilities
という名前で3つのレイヤーを宣言してみます。
<style>
@layer common, page, utilities;
</style>
<link href="common.css" rel="stylesheet" />
<link href="page.css" rel="stylesheet" />
<link href="utilities.css" rel="stylesheet" />
どのレイヤーが優先されるかは宣言順に依存し、よりあとに宣言されたレイヤーが優先されます。例の場合、常にcommon
レイヤーの内容よりpage
レイヤーの内容が優先され、それよりもさらにutilities
レイヤーの内容が優先されます。
続いて、各CSSファイルの内容をレイヤーに属する形に書き換えます。@layer
を用いて、先に宣言したレイヤー名と同じ名前でCSSを定義します。あわせて、ユーティリティCSSからは!important
を削除します。
/* 共通CSS (common.css) */
@layer common {
a {
font-size: 12px;
padding: 8px;
}
a:hover {
font-size: 12px;
color: red;
}
}
/* ページ固有CSS (page.css) */
@layer page {
a {
font-size: 14px;
border: 1px solid gray;
}
}
/* ユーティリティCSS (utilities.css) */
@layer utilities {
.util-text-large {
font-size: 24px;
}
.util-padding {
padding: 16px;
}
}
これでCascade Layersを用いた実装は完了です。ブラウザで確認しても、Cascade Layersを用いない場合と同じ見た目を再現できているはずです。
CSS詳細度とは別にレイヤーでの優先順位を定義しているため、常にユーティリティCSS → ページ固有CSS → 共通 CSSの順に優先されます。これにより、共通CSSにはa:hover
に対するスタイルを含めていますが、常にページ固有CSSのスタイルが優先されるため、ホバー時の意図しないフォントサイズの変化もありません。
仮に共通CSSで#special a
のようにIDセレクタなどを用いた詳細度の高いCSSを定義しても、それを意識せずにページ固有CSSやユーティリティCSSで確実に上書きできます。!important
に頼る必要もありません。また、link
要素の順序を入れ替えたとしても見た目は変わりません。
なお、外部CSSを@import
で取り込む際にもレイヤーを指定できます。外部CSSを優先順位の低いレイヤーで利用することで、外部CSS内にどのような詳細度指定を含んでいても、それを意識することなく独自スタイルで確実に上書きできます。
/* 外部CSSを"external"レイヤーで取り込む例 */
@import "https://example.com/external.css" layer(external);
暗黙のレイヤー
Cascade Layersでは、よりあとに宣言されたレイヤーが優先されると解説しました。では、レイヤーを使わないスタイルが同時に存在していた場合、どういった優先順位となるのでしょうか?
次のように、レイヤーの外でスタイルを定義して確認してみます。
<style>
@layer common, page, utilities;
</style>
<style>
/* レイヤー外で定義されたスタイル */
a {
color: green;
font-size: 12px;
padding: 12px;
}
</style>
<link href="common.css" rel="stylesheet" />
<link href="page.css" rel="stylesheet" />
<link href="utilities.css" rel="stylesheet" />
すると、新たに追加したレイヤー外のスタイルが優先して適用され、共通CSS・
これは、暗黙のレイヤーと呼ばれるものに起因する挙動です。明示的なレイヤーに含めずに宣言したスタイルは、すべて暗黙のレイヤーに属するとみなされます。そしてこの暗黙のレイヤーは、レイヤーの宣言順においては常に一番最後に宣言されているとみなされます。Cascade Layersでは、よりあとに宣言されたレイヤーが優先されるため、例の場合ではレイヤーは次の優先順位となります。
- 暗黙のレイヤー
- utilitiesレイヤー
(ユーティリティCSS) - pageレイヤー
(ページ固有CSS) - commonレイヤー
(共通CSS)
これはすでにスタイルが存在している環境に対してCascade Layersを新規で導入する際には知っておいたほうがよい挙動です。
たとえば、今回の例における共通CSSのような、全体のベースとなるスタイルを定義する際には、Cascade Layersでレイヤー上に宣言することで暗黙の最終レイヤーより優先順位が低くなります。既存のスタイルは暗黙の最終レイヤーに属するため、自然な形で上書きして使うことができます。
しかし、ユーティリティCSSのような
レイヤーにどういった役割を求めるかによって対応順が変わる点に注意が必要となります。
ブラウザでのサポート状況
今回紹介した機能について、本記事掲載時点でのメジャーブラウザにおける利用可能バージョンは次のとおりです。これ以降のバージョンであれば利用可能です。
機能 | Chrome | Edge | Safari | Firefox |
---|---|---|---|---|
Cascade Layers | 99 | 99 | 15. |
97 |
まとめ
Cascade Layersを用いると、役割や目的に応じてCSSを分割し、かつレイヤー間での優先順位を明確かつ宣言的にコントロールできます。単純に上書きするためだけに利用するのではなく、レイヤーごとにどういった役割を持つかを意識しての設計が重要となります。うまく活用できると、従来よりもより保守しやすい形でスタイルを定義できるようになるでしょう。