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

Container Queries/祖先要素に応じたCSSの切り替え [CSS Modern Features no.2]

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

今回のテーマはContainer Queriesです。

Container Queriesは、祖先要素として存在するコンテナのスタイルに応じてCSSを適用するための機能です。利用時は@containerで宣言します。

従来でもメディアクエリでブラウザのビューポート幅などに応じたCSS適用は可能でしたが、あくまでもブラウザやウィンドウ全体のスタイルに依存するものでした。Container Queriesでは画面の特定の範囲を「コンテナ」として定義し、コンテナのスタイルに応じたCSSを適用できる点が、従来の手法との大きな違いとなります。

Container Queriesの利用例/表示箇所で見た目が変化するカードコンポーネント

カード形式で表示するコンポーネントの実装を例にContainer Queriesの利用例を見てみましょう。

次のようなスタイリングを想定します。

  • カード形式のコンポーネントを表示し、スタイルを適用したい
  • コンポーネントは、表示する箇所の幅が400px以下であれば見た目を変える
    • 400pxを超える場合は画像と内容を横並びで表示する
    • 400px以下の場合は画像と内容を縦並びで表示する

カードのHTMLは次のような内容です。

<div class="card">
  <div>
    <img class="avatar" src="./avatar.png" />
  </div>
  <div class="content">
    <div class="name">mugi_uno</div>
    <div class="description">
      Cybozu, Inc. <br />
      Frontend Expert Team <br />
      @mugi_uno <br />
    </div>
  </div>
</div>

コンポーネントを配置する画面全体は次のような仕様とします。

  • サイドバーとメインコンテンツの2カラムで構成されている
  • サイドバーは160pxで固定
  • メインコンテンツはウィンドウのサイズに応じて可変

画面全体のHTMLとCSSの一部を抜粋すると、次のような内容です。

<body>
  <nav>
    <h4>Side Bar</h4>
    <div class="card tiny">
      <!-- (省略)cardのコンテンツ -->
    </div>
  </nav>
  <main>
    <h4>Main Contents</h4>
    <div class="card">
      <!-- (省略)cardのコンテンツ -->
    </div>
  </main>
</body>
body {
  display: grid;
  grid-template-columns: 160px 1fr;
}

カードコンポーネントをサイドバーとメインコンテンツの両方に配置すると、ウィンドウ幅が広い場合は次のような表示を想定します。

図1 カードコンポーネントのイメージ

一方で、ウィンドウサイズを狭めてメインコンテンツの幅が400px以下となった場合は、サイドバーと同様の見た目となります。次のようなイメージです。

図2 ウィンドウを狭めた場合の表示例

従来の方法/メディアクエリを用いた場合

まず、Container Queriesを用いずに、従来はどう実装していたか確認してみましょう。

メディアクエリを用いると、ビューポート幅に応じてCSSを適用できます。しかしサイドバーの幅は常に固定幅ですので、ビューポート幅にかかわらずスタイルを適用する必要があります。こういった場合、class属性などを用いてスタイルを切り替える方法がよく用いられます。

次の例ではtinyクラスを付与してスタイルを切り替えています。

<body>
  <nav>
    <h4>Side Bar</h4>
    <div class="card tiny">
      <!-- (省略)cardのコンテンツ -->
    </div>
  </nav>
  <main>
    <h4>Main Contents</h4>
    <div class="card">
      <!-- (省略)cardのコンテンツ -->
    </div>
  </main>
</body>
/* デフォルトのスタイル */
.card {
  /* カード内容を横に並べて表示 */
  display: grid;
  grid-template-rows: 240px;
  grid-template-columns: 240px 1fr;
}
.card.tiny {
  /* カード内容を縦に並べて表示 */
  grid-template-rows: 160px 1fr;
  grid-template-columns: 1fr;
}

一方、メインコンテンツ側では、ビューポートに応じてスタイルを切り替える必要があります。しかし、サイドバーが常に一定幅を確保するため、それを考慮してメディアクエリを定義する必要があります。サイドバーの幅が160pxのため、次のような定義となります。

/* 切り替え基準の400px+サイドバーの幅160px */
@media screen and (width < calc(400px + 160px)) {
  .card {
    /* カード内容を縦に並べて表示 */
    grid-template-rows: 160px 1fr;
    grid-template-columns: 1fr;
  }
}

※余談ですが、メディアクエリに利用しているwidth < calc(400px + 160px)はRange Syntaxと呼ばれるメディアクエリのモダンな記法です。

メディアクエリを用いた実装の課題

メディアクエリを用いて、ひとまず期待どおりの見た目を実現できました。しかし同時に次のような課題も抱えています。

  • メインコンテンツ側のCSSで、常にサイドバーの幅を意識する必要がある
  • tinyクラスの付与に誤りがあると意図しない見た目となる
  • tiny時のCSSとメディアクエリ指定のCSSで内容が重複する

特に、現状の実装ではサイドバーの幅がメインコンテンツのメディアクエリ指定に影響を及ぼします。意図しない表示崩れを引き起こす要因となるため、スタイルの追加や変更時には注意が必要となります。

Container Queriesを用いた場合

では、Container Queriesではどうなるか見てみましょう。まず、サイドバーとメインコンテンツのレイアウトと幅を指定したうえで、コンテナとして扱われるようcontainer-typeを指定します。

body {
  /* サイドバーとメインコンテンツを横並びにして幅を指定 */
  display: grid;
  grid-template-columns: 160px 1fr;
}

/* サイドバー */
nav {
  container-type: inline-size;
}
/* メインコンテンツ */
main {
  container-type: inline-size;
}

container-typeの値には、Container Queriesが何を基準に動作するかを指定します。inline-sizeの場合、コンテナのインラインサイズに応じて計算されます。

続いて、カードコンポーネントのCSSでは、デフォルトのスタイルに加えて、@containerでコンテナに応じたスタイルを記述します。

/* デフォルトのスタイル */
.card {
  /* カード内容を横に並べて表示 */
  display: grid;
  grid-template-rows: 240px;
  grid-template-columns: 240px 1fr;
}

/* コンテナに応じたスタイル */
@container (width < 400px) {
  .card {
    /* カード内容を縦に並べて表示 */
    grid-template-rows: 160px 1fr;
    grid-template-columns: 1fr;
  }
}

@container (width < 400px)「最も近い祖先のコンテナのサイズが400px以下の場合」のみ適用されるスタイルです。サイドバーのコンテナは160px固定であり、400px以下に該当するため常にこのスタイルが適用されます。メインコンテンツのコンテナでは、ウィンドウを狭めて幅を400px以下にした場合にのみ適用されます。

実装はこれだけで完了です。⁠400px以下の場合はスタイルを切り替える」という仕様がCSS上で明示的に表現されており、定義の重複もありません。仮にほかの場所で表示が必要になっても、祖先要素をコンテナとして宣言するだけで、コンテナの幅に応じて自動的にカードコンポーネントの見た目が切り替わります。

なお、Container Queriesと組み合わせて使う機能として、コンテナのサイズに応じた数値単位であるcqwcqhといったものも存在します。たとえば「コンテナの幅に対して10%の幅にしたい」といったケースでは10cqwで表現でき、よりコンテナに応じて柔軟にスタイルを適用できます。

container-nameによるコンテナ名の指定

Container Queriesで注意すべき点として、コンテナのネストが挙げられます。

コンテナ要素がネストした場合、祖先をたどり最も近いコンテナが利用されます。しかし、状況によっては「ルート要素に近いコンテナを参照してほしい」といったケースも考えられます。そのような場合、Container Queriesではcontainer-nameプロパティを使うことで、参照するコンテナと@containerで宣言されたCSSルールを名前で明示的に紐づけることができます。

さきほどの例では、次のように修正することでコンテナ名を指定できます。

nav {
  container-type: inline-size;
  /* コンテナ名を指定 */
  container-name: layout;
}

main {
  container-type: inline-size;
  /* コンテナ名を指定 */
  container-name: layout;
}

/* layout コンテナでのみ適用 */
@container layout (width < 400px) {
  /* 省略 */
}

カードコンポーネントはレイアウトに関係する要素のサイズに応じてスタイルを切り替えたいので、"layout"というコンテナ名を定義しています。これにより、container-nameで"layout"を明示的に指定しない限り、カードコンポーネントのコンテナとしては利用されません。

たとえば次のようにmain要素の中に.sub-containerクラスを持つ要素をネストし、幅は50%のコンテナとして配置したとします。しかし、.sub-containerを対象としたCSS定義にはcontainer-nameが含まれないため、カードコンポーネントのコンテナとしては機能しません。結果として、main要素の幅だけを参照してContainer Queriesが適用されます。

.sub-container {
  width: 50%;
  container-type: inline-size;
}
<main>
  <h4>Main Contents</h4>
  <!-- カードコンポーネントのコンテナとしては機能しない -->
  <div class="sub-container">
    <div class="card">
      <!-- (省略)cardのコンテンツ -->
    </div>
  </div>
</main>

もし@containerでのContainer Queries宣言時にcontainer-nameを利用しなかった場合、.sub-containerクラスを持つdiv 要素もカードコンポーネントのコンテナとして機能します。結果として、50%の幅も加味してContainer Queriesが適用されるため、よりウィンドウ幅が広い状態でも見た目が切り替わるような挙動となります。

Container Queriesを多くのシチュエーションで利用する際には、コンテナの衝突を回避するために極力container-nameを付与しておくほうが安全でしょう。

Container Style Queries

Container Queriesには実は複数の機能が存在します。ここまでに紹介した機能は、実は厳密にはContainer Queriesの中でもContainer Size Queriesと呼ばれ、コンテナのサイズに応じてスタイルを適用するものです。

一方で、Container Style Queriesと呼ばれる、コンテナが特定のスタイルを持つかを判定してスタイルを適用する機能が存在します。

Container Style Queriesの利用例/テーマの切り替え

Container Style Queriesの利用例のひとつとして、テーマの切り替えが挙げられます。実際の例を見ながらどういった機能か確認してみましょう。

次のようなスタイリングを想定します。

  • コンテナでは2つのテーマに応じて背景色が黒か白で切り替わる
  • コンテナの中にはヘッダが存在する
  • コンテナの背景色が黒の場合:ヘッダの文字色を白にする
  • コンテナの背景色が白の場合:ヘッダの文字色を黒にする

HTMLは次のような内容です。

<div class="dark-container">
  <h2>Dark theme title</h2>
</div>
<div class="light-container">
  <h2>Light theme title</h2>
</div>

このとき、Container Style Queriesを用いると次のようにスタイルを定義できます。

@container style(--bg-color: black) {
  h2 {
    color: white;
  }
}
@container style(--bg-color: white) {
  h2 {
    color: black;
  }
}

.dark-container {
  --bg-color: black;
  background-color: var(--bg-color);
}
.light-container {
  --bg-color: white;
  background-color: var(--bg-color);
}

実行結果は次の通りです。コンテナの背景色に応じてh2要素の文字色が切り替わっていることが確認できます。

図3 Container Style Queriesによるテーマ切り替えのイメージ

@container style(--bg-color: black)のように、style()にカスタムプロパティを指定することで、コンテナのカスタムプロパティの値を参照し、合致する場合にのみスタイルが適用されます。なお、Container Style Queriesにおける「コンテナ」は自動的に祖先要素を参照して判定されるため、Container Size Queriesのようにcontainer-typeの指定は必要ありません。

上述の例ではコンテナの背景色に応じてスタイルを適用しましたが、カスタムプロパティに任意の文字列を指定して複数のテーマに対応させる、といったことも可能です。

例として、--themeというカスタムプロパティを用いて、3種類のテーマを切り替えてみましょう。

CSSは次のような内容です。

@container style(--theme: dark) {
  h2 {
    color: white;
  }
}
@container style(--theme: light) {
  h2 {
    color: black;
  }
}
@container style(--theme: colorful) {
  h2 {
    color: orange;
  }
}

.dark-container {
  --theme: dark;
  background-color: black;
}
.light-container {
  --theme: light;
  background-color: white;
}
.colorful-container {
  --theme: colorful;
  background-color: green;
}

HTMLにも全テーマに対応する表示を追加します。

<div class="dark-container">
  <h2>Dark theme title</h2>
</div>
<div class="light-container">
  <h2>Light theme title</h2>
</div>
<div class="colorful-container">
  <h2>Colorful theme title</h2>
</div>

実行結果は次の通りです。3種類のテーマで切り替わっていることが確認できます。

図4 Container Style Queriesによる複数テーマ切り替えのイメージ

上記では単純なテーマの切り替えを例として挙げていますが、Container Style Queriesではカスタムプロパティをどのような役割として定義するかによって、さまざまな目的に利用できます。たとえば、テーマ切り替え以外に次のような使い方も考えられるでしょう。

  • gridの行列数に応じたグリッドレイアウトの調整
  • PC・モバイルに応じたスタイリング
  • 言語に応じたスタイルの切り替え
  • A/Bテストでの利用

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

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

機能 Chrome Edge Safari Firefox
Container Size Queries 106 106 16 110
container-name 105 105 16 110
Container Style Queries 111 111 18 (未対応)

まとめ

今回はContainer Queriesを紹介しました。

昨今のWebフロントエンドでのUI開発ではコンポーネント指向が一般的となっています。それに伴いスタイリングもコンポーネントとして管理する需要が高まっていますが、Container Queriesを知っておくとJavaScriptに頼らずに多くのシチュエーションに対応可能な汎用性の高いコンポーネントも実現できるため、ぜひチャンスがあれば活用してみてください。

おすすめ記事

記事・ニュース一覧