乗りこなせ! モダンフロントエンド

Cascade Layers/レイヤーによる優先順位の制御 [CSS Modern Features no.3]

こんにちは! サイボウズ フロントエンドエキスパートチームの麦島です。

今回取り上げるCSSの機能はCascade Layersです。

Cascade Layersは、⁠レイヤー」と呼ばれる階層を独自に定義することで、CSS適用の優先順位をコントロールする機能です。利用時は@layerを用いて宣言します。

Cascade Layersと優先順位

CSSを適用する際、CSSセレクタを用いた詳細度での優先順位のコントロールが広く利用されています[1]

しかし、新たにCascade Layersを組み合わせた場合には次の順番で適用されます。

  1. Cascade Layers
  2. CSSセレクタでの詳細度指定

Cascade LayersがCSSセレクタよりも優先して適用される点が重要なポイントです。これにより、役割や目的に応じたCSS設計の大きな助けとなります。

実際に例を見てみましょう。

Cascade Layersの利用例/役割に応じたCSSファイルの分割

CSSの定義がある程度の規模を超えると、すべてを単一ファイルに定義して管理するのは困難となります。対策として、役割に応じてCSSファイルを分割して管理する手法が広く用いられています。

今回は、CSSを「共通CSS」⁠ページ固有CSS」⁠ユーティリティCSS」の3ファイルに分割して定義するケースを例に考えてみます。役割はそれぞれ次のとおりです。

共通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>

最終的に期待する見た目は次のとおりです。

図1 実装する画面のイメージ

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;
}

結果として、⁠ホバー時フォントサイズが少し小さくなってしまう」といった挙動となります。

図2 ホバー時にフォントサイズが少し小さくなってしまう

ひとつの回避策として:where()擬似クラスを利用して共通CSSの詳細度を0とする方法が考えられます。しかし、サードパーティ製ライブラリなどの外部CSSを利用している場合には該当ファイル内容を独自で書き換える必要が生じてしまい、リスクも伴い対応が難しくなります。

他にも、ユーティリティCSSを確実に適用させるため!importantを利用している点も問題となりえるでしょう。共通CSSやページ固有CSSでどのような詳細度が指定されるかは不明であり、確実に上回るためには!importantを付与するか、高い詳細度を持つ必要が生じます。

また、分割したCSS間で詳細度が同じ場合には、暗黙的にlink要素の順序にも依存しています。link要素の順番を入れ替えた場合に意図せず見た目が変化する恐れがあります。

Cascade Layersによる役割の分割

では、Cascade Layersがどう問題を解決するか見てみましょう。

Cascade Layersでは@layerでレイヤーを宣言します。ここではそれぞれcommonpageutilitiesという名前で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のスタイルが優先されるため、ホバー時の意図しないフォントサイズの変化もありません。

図3 Cascade Layersによりホバー時でもフォントサイズが変わらない

仮に共通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・ページ固有CSS・ユーティリティCSSの内容は上書きされてしまいました。

図4 レイヤー外のスタイルが優先して適用される

これは、暗黙のレイヤーと呼ばれるものに起因する挙動です。明示的なレイヤーに含めずに宣言したスタイルは、すべて暗黙のレイヤーに属するとみなされます。そしてこの暗黙のレイヤーは、レイヤーの宣言順においては常に一番最後に宣言されているとみなされます。Cascade Layersでは、よりあとに宣言されたレイヤーが優先されるため、例の場合ではレイヤーは次の優先順位となります。

  1. 暗黙のレイヤー
  2. utilitiesレイヤー(ユーティリティCSS)
  3. pageレイヤー(ページ固有CSS)
  4. commonレイヤー(共通CSS)

これはすでにスタイルが存在している環境に対してCascade Layersを新規で導入する際には知っておいたほうがよい挙動です。

たとえば、今回の例における共通CSSのような、全体のベースとなるスタイルを定義する際には、Cascade Layersでレイヤー上に宣言することで暗黙の最終レイヤーより優先順位が低くなります。既存のスタイルは暗黙の最終レイヤーに属するため、自然な形で上書きして使うことができます。

しかし、ユーティリティCSSのような「あとから強いルールで個別に上書きをしたい」といった用途を考えた場合、これだけをCascade Layersで宣言したとしても、既存のスタイルはすべて暗黙のレイヤーに属しているため、優先順位で上回ることはできません。対応するためには、既存スタイルも明示的なレイヤーに属する形に書き換えて宣言順を変更する必要があります。

レイヤーにどういった役割を求めるかによって対応順が変わる点に注意が必要となります。

ブラウザでのサポート状況

今回紹介した機能について、本記事掲載時点でのメジャーブラウザにおける利用可能バージョンは次のとおりです。これ以降のバージョンであれば利用可能です。

機能 Chrome Edge Safari Firefox
Cascade Layers 99 99 15.4 97

まとめ

Cascade Layersを用いると、役割や目的に応じてCSSを分割し、かつレイヤー間での優先順位を明確かつ宣言的にコントロールできます。単純に上書きするためだけに利用するのではなく、レイヤーごとにどういった役割を持つかを意識しての設計が重要となります。うまく活用できると、従来よりもより保守しやすい形でスタイルを定義できるようになるでしょう。

おすすめ記事

記事・ニュース一覧