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

CSS Nesting Module/CSSの入れ子指定 [CSS Modern Features no.4]

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

CSS Modern Features、今回取り上げるCSSの機能はCSS Nesting Moduleです。

CSS Nesting Moduleは、CSS定義のネスト(入れ子)記述を可能とする新しいCSS構文です。CSSのネストはSassやPostCSSといったCSSプリプロセッサ経由で変換することで似た構文が使えるため、そちらをすでに利用したことのある人もいるかもしれません。

次のコードはCS Nesting Moduleの記述例です。

/* 
  .mainクラスを持つ要素の
  子孫のa要素のカラーを変更
*/

/* 従来の記述 */
.main a {
  color: red;
}

/* CSS Nesting Module での例 */
.main {
  a {
    color: red;
  }
}

CSS Nesting Moduleの利用例/階層の深い要素へのスタイル指定

CSS Nesting Moduleが活用できる例として、階層が深い構造に対してスタイルを適用するケースが考えられます。

次のような、メニュー表示におけるHTMLへのスタイル指定を例に見てみましょう。

<main>
  <ul>
    <li>
      <a href="/top">Top<span class="icon"></span></a>
    </li>
    <li>
      <a href="/articles">Articles<span class="icon"></span></a>
    </li>
    <li>
      <a href="/search">Search<span class="icon"></span></a>
    </li>
  </ul>
</main>

mainulliaspanの階層での入れ子構造となっています。このHTMLに次のスタイルを適用したいと想定します。

  • スタイルは同様のマークアップ構造mainulaspanの場合にのみ適用する
  • a要素がホバーされた場合にはスタイルを切り替える
  • .iconクラスを持つspan要素は、親のa要素がホバーされた場合にのみ表示する

実際の見た目は次のイメージです。

図1 ネストした要素へのスタイル適用

これをCSS Nesting Moduleを用いない形でCSSで実装すると、次のようなコードになります。

/* a要素の基本スタイル */
main > ul > li > a {
  border: 2px solid transparent;
  padding: 4px;
}
/* a要素がホバーされた場合のスタイル */
main > ul > li > a:hover {
  border-color: blue;
  font-weight: bold;
}
/* .icon要素は普段は非表示 */
main > ul > li > a > span.icon {
  display: none;
}
/* 親のa要素がホバーされた場合.icon要素を表示 */
main > ul > li > a:hover > span.icon {
  display: inline;
}

CSSの内容を見てみると、階層が深いことにより定義の重複が多く発生しています。main > ul > li > aの部分はすべての定義で重複しており、ここでたとえばmain要素の配下はsectionで囲いたい」といった変更をしたい場合には多くの箇所で修正が必要となります。ほかにも、CSS 全体のサイズもネストの深さに比例して肥大化していくといった問題も抱えています。

CSS Nesting Moduleを用いた場合

それでは、CSS Nesting Moduleで同じスタイルを記述した場合の例を見てみましょう。

main > ul > li > a {
  /* a要素の基本スタイル */
  border: 2px solid transparent;
  padding: 4px;

  &:hover {
    /* a要素がホバーされた場合のスタイル */
    border-color: blue;
    font-weight: bold;

    /* 親のa要素がホバーされた場合.icon要素を表示 */
    > span.icon {
      display: inline;
    }
  }

  /* .icon要素は普段は非表示 */
  > span.icon {
    display: none;
  }
}

main > ul > li > aの部分の重複が排除されています。HTMLのマークアップ構造と同じようにCSS上でもネストして表現できるため、より直感的に記述できます。

なお、セレクタでは通常どおり擬似クラスを使えるため、:is():where()などと組み合わせることで、さらに柔軟に階層構造を表現できます。たとえば、a要素に加えてbutton要素でも同等のスタイルを適用したい」といったケースでも、次のように記述できます。

main > ul > li > :is(a, button) {
  /* 省略 */
}

Nesting Selector&

先述の例の中で、ホバー用スタイルにおいて&:hoverといった記述をしています。

ここでの&は、Nesting Selectorと呼ばれるCSS Nesting Moduleで利用可能な新しいCSSセレクタです。Nesting Selectorは、入れ子のスタイルの中で、親ルール自体のセレクタを参照したい場合に使用します。

具体的には、例のように:hoverなどの擬似クラスが付与された要素へのスタイル適用での利用が代表的です。この場合、もしNesting Selectorを省略した場合は、単純に子孫要素のセレクタとして解釈され、大きく異なる挙動となります。

次のようなケースを考えてみます。

/* Nesting Selector を使った場合 */
.link-a {
  &:hover {
    color: red;
  }
}

/* Nesting Selector を使わない場合 */
.link-b {
  :hover {
    color: red;
  }
}

これは、それぞれ次のCSSと同義です。

.link-a:hover {
  color: red;
}

.link-b :hover {
  color: red;
}

擬似クラスの手前にスペースが入っているかどうかが違いです。Nesting Selectorを使った場合については、.link-aの要素がホバーされた場合のスタイル」であるのに対し、Nesting Selectorを使わない場合では.link-bのいずれかの子孫要素がホバーされた場合のスタイル」となり、大きく意味合いが異なります。仮にホバー時に.link-b自体のスタイルの切り替わりを期待していた場合には、意図しない挙動となります。

At-RulesとCSS Nesting Module

At-Rulesとは、メディアクエリでの@mediaやCascade Layersでの@layerなど、@から始まるCSS機能のことを指します。

CSS Nesting Moduleでは、次のAt-Rulesをサポートしており、ネストの中で用いることができます。

  • @media
  • @container
  • @supports
  • @layer
  • @scope

今回は、本連載で過去に取り上げた@container@layerについて、CSS Nesting Moduleと組み合わせた場合の動作を見てみましょう。

@containerとCSS Nesting Module

@containerは、Container Queriesで用いられるAt-Rulesです[1]

Container Queriesを用いて、次のスタイルを記述してみます。

  • コンテナサイズが100px未満の場合には文字色を赤にする
  • コンテナスタイル(Container Style Queries)が"my-style"の場合には文字サイズを大きくする
  • 上記をともに満たす場合には、文字色を青くし、文字サイズをさらに大きくする

HTMLおよびコンテナに該当する要素に適用するCSSは次の内容です。サイズと--styleの値が異なる4つのコンテナを定義しています。

<div class="container container-A">
  <div class="content">Text</div>
</div>
<div class="container container-B">
  <div class="content">Text</div>
</div>
<div class="container container-C">
  <div class="content">Text</div>
</div>
<div class="container container-D">
  <div class="content">Text</div>
</div>

まず、CSS Nesting Moduleを使わない場合、期待するスタイルは次のように表現できます。

/* コンテナの定義 */
.container {
  container-type: inline-size;
  margin: 8px;
  border: 1px solid #000;
}
.container-A {
  width: 100px;
}
.container-B {
  width: 80px;
}
.container-C {
  width: 100px;
  --style: my-style;
}
.container-D {
  width: 80px;
  --style: my-style;
}

/* コンテナ内のコンテンツのスタイル */
.content {
  color: black;
  font-size: 16px;
}
@container (width < 100px) {
  .content {
    color: red;
  }
}
@container style(--style: my-style) {
  .content {
    font-size: 24px;
  }
}
@container (width < 100px) and style(--style: my-style) {
  .content {
    color: blue;
    font-size: 32px;
  }
}

すると、次のような見た目となります。

図2 Container Queriesを用いたスタイル適用

期待通り動作していますが、.contentのスタイル定義は重複しています。また、各Container Queriesと.contentの関連がわかりづらいのも課題となり得ます。定義場所が離れていたり、@container内に.content以外のスタイルを定義したりしてしまうと、関連がより一層あいまいとなり、保守性が低下します。

では、CSS Nesting Moduleを使って表現した例を見てみましょう。

/* コンテナの定義 */
/* 〜同様のため省略〜 */

/* コンテナ内のコンテンツのスタイル */
.content {
  color: black;
  font-size: 16px;

  @container (width < 100px) {
    color: red;
  }
  @container style(--style: my-style) {
    font-size: 24px;

    @container (width < 100px) {
      color: blue;
      font-size: 32px;
    }
  }
}

CSS Nesting Moduleを用いない場合と比較してみると、.contentの記述やstyle(--style: my-style)の定義の重複が排除され、簡潔に表現できています。また、.contentのスタイル定義の中にすべての@container定義が集約されており、要素とContainer Queriesの関連が明確になっています。コンポーネントのスタイル定義では関連するスタイルを一ヵ所に集約しておくことで責務が明示的になり保守性が高まるため、その点でCSS Nesting Moduleと組み合わせるメリットはあると言えるでしょう。

@layerとCSS Nesting Module

@layerは、Cascade Layersで用いられるAt-Rulesです[2]

Cascade Layersでは、.でレイヤー名をつないで宣言することでサブレイヤーを定義できます。

たとえば「ベースのレイヤーは先頭で宣言したいが、その中でさらにグローバルスタイルとデフォルトスタイルで分離したい」といったケースにはこの機能が活用できます。次の例では、baseレイヤーのサブレイヤーとしてbase.globalレイヤーとbase.defaultレイヤーを定義しています。

@layer base.global {
  * {
    color: gray;
  }
  a:hover {
    color: blue;
  }
}
@layer base.default {
  a {
    color: red;
  }
}

これらはあくまでもbaseレイヤーに属するサブレイヤーであり、baseレイヤー自体の優先度にも大きく影響を受けます。仮にあらかじめ@layer base, page;といった形でレイヤーを宣言していた場合、レイヤーの優先順序は次のとおりです。

  1. pageレイヤー
  2. baseレイヤー
  3. base.defaultサブレイヤー
  4. base.globalサブレイヤー

base.globalレイヤーとbase.defaultレイヤーに宣言されたスタイルよりもpageレイヤーに宣言されたスタイルのほうが優先されます。

このCascade Layersでのサブレイヤーの宣言について、CSS Nesting Moduleを用いると次のように記述できます。

@layer base {
  @layer global {
    * {
      color: gray;
    }
    a:hover {
      color: blue;
    }
  }

  @layer default {
    a {
      color: red;
    }
  }
}

.でつないだ形での宣言よりも、レイヤー・サブレイヤーの包括関係が明確となり、base.を複数回記述する必要もありません。筆者の意見としては、CSS Nesting Moduleが利用できる環境であれば積極的に活用していくほうが望ましいと考えています。

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

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

機能 Chrome Edge Safari Firefox
CSS Nesting Module 120 120 17.2 117

なお、今回紹介したCSS Nesting Moduleは、本記事掲載時点では仕様的にはWorking Draftであり、今後仕様が変更される可能性がある点についてはご留意ください。

まとめ

CSS Nesting Moduleを用いると、より関連性の近いスタイルの依存・包括関係が明示的となり、かつ記述の重複が削減され保守性の向上も期待できます。SassやPostCSSの類似機能を用いて恩恵を受けるケースも多くありましたが、それが標準で使えるようになるのはとてもありがたいですね。今後標準化が進むにつれ広く利用される可能性が高い機能と考えられるため、ぜひ押さえておきましょう。

おすすめ記事

記事・ニュース一覧