2010 年に公開された Ethan Marcotte 氏の記事1でレスポンシブウェブデザイン(RWD)の手法が提唱されて以来、レイアウト幅に応じてスタイルを変更する方法を実現するには、メディアクエリ(@media)を使用するのが一般的でした。

そして 2023 年現在、コンテナクエリ(@container)が主要なブラウザでサポートされ、RWD の考え方が大きく変化しようとしています。

すでに多くの記事で解説されていますが、コンテナクエリの仕様は複雑です。この記事では、コンテナクエリの例を挙げながら、実際にどのような使い方ができるのか、使用する際にどのような懸念点が考えられるかといった点について考えていきます。

コンテナクエリは主要なブラウザでサポートされていますが 2、W3C の仕様では草案(Working Draft)段階のため、今後、実装方法や機能に変更が発生する可能性がある点を留意しておく必要があります。

コンテナクエリは「CSS Containment Module Level 3」に含まれていますが、このモジュールには containcontent-visiblity といった、おもにレンダリングの最適化に関連するプロパティが属しています。

「Containment」は「封じ込め」ですので、周囲の要素に影響を及ぼさない独立した要素に対して、封じ込めたスタイルを適用するイメージで考えるとよいかもしれません。

なお、W3C のドキュメントではコンテナクエリの基準となる要素のことを「Query Container(クエリコンテナ)」と表記されていますが、コンテナクエリと似ていてまぎらわしいので、この記事では「基準コンテナ」と呼ぶことにします。

本記事では、親要素や祖先要素のサイズに応じて子孫要素のスタイルを切り替える「Size Queries」にフォーカスします。スタイルに応じて子孫要素のスタイルを切り替える「Style Queries」については詳しくは取り上げません。

それではまず、コンテナクエリの例を見てみましょう。

以下はサンプル用のオーディオプレーヤーです(音声は再生できません)。スライダーを動かすと、プレーヤーのコンテナの幅が変わります。

200px
アルバムカバー

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem

0:00 4:33

スライダーを操作してプレーヤーの幅を変更すると、以下の 3 つの変化が起こります。

  1. プレーヤーのレイアウト
  2. ボタンのサイズ
  3. テキストのサイズ

1. プレーヤーのレイアウト

見出し「1. プレーヤーのレイアウト」

まず、一番大きな変化ですが、スライダーを動かして幅を 460px 以上にすると、プレーヤーのレイアウトが縦長から横長に切り替わります。

プレーヤーの幅 200px と 460px で比較した図
460px をポイントにプレーヤーのレイアウトが切り替わる

これは、プレーヤーを囲う要素(.player-container)を基準に、コンテナクエリを指定しているためです。以下にコードを抜粋します。

<!-- HTML -->
<div class="player-container">
  <div class="player">
    <figure class="cover">
      <!-- 画像 -->
    </figure>
    <div class="ui-container">
      <div class="ui">
        <!-- UI -->
      </div>
    </div>
  </div>
</div>

<!-- CSS -->
<style>
.player-container {
  container: player / inline-size;
}
@container player (inline-size >= 460px) {
  .player {
    display: grid;
    grid-template-columns: 200px auto;
  }
}
</style>

まず、container: player / inline-size でコンテナ名とコンテナタイプを指定しています。

そのあとに指定している、@container で、基準コンテナが inline-size >= 460px のときに、子要素 .player のスタイルを変更しています。

CSS については、プロパティ以降でも取り上げます。

320px を超えたあたりで再生ボタンとその左右のボタンのサイズが切り替わります。

しかし、460px でプレーヤーが横長レイアウトになると、再びボタンのサイズが小さくなりますが、520px を超えると再び大きくなります。

プレーヤーの幅 200px、320px、460px、520px で比較した図。再生ボタンとその左右のボタンが破線で囲われている
プレーヤーの幅に応じて、ボタンのサイズが切り替わる

これは、基準コンテナをプレーヤーではなく UI 部分に設定しているためです。以下のように .ui-containerinline-size >= 320px のときに、ボタンのサイズを変更しています。

<!-- HTML -->
<div class="player-container">
  <div class="player">
    <figure class="cover">
      <!-- 画像 -->
    </figure>
    <div class="ui-container">
      <div class="ui">
        <!-- UI -->
      </div>
    </div>
  </div>
</div>

<!-- CSS -->
<style>
.ui-container {
  container: ui / inline-size;
}
@container ui (inline-size >= 320px) {
  .play {
    width: 60px;
  }
  :where(.prev, .next) {
    width: 20px;
  }
}
</style>

このように、ひとつのコンポーネントであっても、複数のコンテナクエリを設定することができるので、文脈(そのときの基準コンテナのサイズ)に応じてスタイルを切り替えることができます。

なお、container-name 自体は任意で、指定しない場合には祖先要素のなかの一番近い基準コンテナが対象となります。

ただ、この例のように複数の基準コンテナが存在する場合には、container-name を明示したほうがコードの可読性や予測可能性は高まります。

テキストのサイズも、この .ui-container を基準コンテナとしていますが、ボタンと比較するとなめらかにサイズが変化します。

ここで、さきほどのサンプルから画像要素を取り除き UI のみにします。この状態でスライダーを動かすと、その変化がわかりやすいと思います。

200px

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem

0:00 4:33

このテキストサイズのなめらかな変化は「Fluid Typography」と呼ばれる手法を用いています。従来はビューポート幅に対して相対的にフォントサイズを変更することがありましたが、この例では基準コンテナの幅を起点としています。

対象となる部分の CSS を抜き出します。

CSS の抜粋
/* 基準となるフォントサイズの指定 */
.ui {
  --font-size-m: calc(1rem * 18 / 16);
  --font-size-s: calc(1rem * 14 / 16);
}
.title {
  --font-size: var(--font-size-m);
}
.artist {
  --font-size: var(--font-size-s);
}
.time {
  --font-size: var(--font-size-s);
}

/* Fluid Typography */
:is(.title, .artist, .time) {
  --font-size-fluid: 4cqi;
  --font-size-diff: 0.22;

  font-size: clamp(
    var(--font-size) - var(--font-size) * var(--font-size-diff),
    var(--font-size-fluid),
    var(--font-size)
  )
}

詳細は割愛しますが、clamp() 関数を使用して、最小値・推奨値・最大値を指定しており、推奨値に cqi を使用しているところがポイントです。

cqi は、基準コンテナの inline-size(書字方向が横書きの場合の横軸のサイズ)で、この値はパーセントです。4cqi であれば、コンテナの幅 4% 分のサイズということになります。

なお、この実装方法は以下の記事を参考にしています。


このように、コンテナクエリでは、親要素や祖先要素のサイズに応じて子孫要素のスタイルを指定できるため、従来のビューポート幅を起点とした切り替えでは不可能だった、文脈に応じたスタイルの切り替えが実現できます。

基準コンテナに使用する CSS プロパティは以下のとおりです。

CSS プロパティ内容
container-typeContainment(封じ込め)の対象軸
container-name基準コンテナの名前
containercontainer-typecontainer-name のショートハンド

container-type では、基準コンテナの対象軸を指定しますが、このプロパティに size もしくは inline-size の値を指定した要素が基準コンテナになります。

  • container-type: size では、横軸・縦軸ともに対象となる
  • container-type: inline-size では、横軸が対象となる(書字方向が横書きの場合)
  • container-type: normal は、デフォルト値でサイズ指定の基準コンテナの対象外

ここで container-type: size を指定すると、横軸・縦軸ともに封じ込めの対象となり、高さを明示しないと領域が確保されなくなります。そのため、container-type: inline-size を指定するケースがほとんどでしょう。

なお、container-type: inline-size であっても、幅を明示しなければならないケースもありますが、こちらは後述します。

container-name は任意ですが、名前を付けることで、@container を使用するときに明示的に紐づけることができます。@container で名前を指定しない場合には、もっとも近い container-type を指定した祖先要素が対象になります。

container はこれらのショートハンドプロパティです。スラッシュ(/)で区切って、container-typecontainer-name をまとめて指定できます。

コンテナクエリの注意点

見出し「コンテナクエリの注意点」

コンテナクエリを使用する際に注意する点としては、基準コンテナに指定した要素自身にはコンテナクエリを指定できないという点です。

基準コンテナ自身にコンテナクエリを指定する例
.container {
  container-type: inline-size;
}

/* 🙅‍♂️ 基準コンテナ自身にコンテナクエリを指定しても無効 */
@container (inline-size >= 600px) {
  .container {
    display: flex;
  }
}

ただし、基準コンテナの擬似要素(::before::after など)には、コンテナクエリを指定できます。

基準コンテナの擬似要素にコンテナクエリを指定する例
.container {
  container-type: inline-size;
}

/* 🙆‍♂️ 基準コンテナの擬似要素に指定したコンテナクエリは有効 */
@container (inline-size >= 600px) {
  .container::after {
    width: 40px;
  }
}

コンテナクエリ(@container)で指定できるサイズ特性は以下のとおりです。

サイズ特性内容
width横幅
height高さ
inline-sizeインラインサイズ(書字方向が横書きの場合、横幅)
block-sizeブロックサイズ(書字方向が横書きの場合、縦幅)
aspect-ratioアスペクト比
orientation縦長(portrait) / 横長(landscape

なお、高さやブロック軸に関連するサイズ特性は、基準コンテナに container-type: size を指定しないと有効にならないので注意が必要です。この条件には、aspect-ratioorientation も含まれます。

コンテナクエリのなかでは、以下の特別な単位が使用できます。

単位内容
cqw基準コンテナの幅の 1%
cqh基準コンテナの高さの 1%
cqi基準コンテナのインラインサイズの 1%
cqb基準コンテナのブロックサイズの 1%
cqmincqicqb の小さい方の値
cqmaxcqicqb の大きい方の値

前述の Fluid Typography の例cqi を使用しましたが、ビューポート幅 vw(もしくは vi)と同じように、コンテナの幅に応じたサイズ指定が可能になります。

コンテナクエリの特長

見出し「コンテナクエリの特長」

以下の 3 つの観点から、コンテナクエリの特長を考えてみます。

  • 関心の分離
  • 再利用性
  • 柔軟性

メディアクエリでは、ブレイクポイント単位でスタイルを指定・追加しています。これは、スタイルを切り替える起点がビューポート幅になり、トップダウンのアプローチといえます。

対して、コンテナクエリではコンテナの幅を起点にスタイルを切り替えることになるので、コンポーネント単位でスタイルが完結します。メディアクエリと比較すると、まさにコンテナを積み上げていくようなボトムアップのアプローチといえます。

メディアクエリとコンテナクエリの比較の図。メディアクエリは、ビューポート幅ごとにスタイルを切り替えるトップダウンのアプローチで、コンテナクエリは、コンテナ幅ごとにスタイルを切り替えるボトムアップのアプローチ

レスポンシブのスタイルをもコンポーネントに封じ込めることができるので、独立性が高まっていると考えられるでしょう(関心の分離)。このことによりコンポーネントの保守性が高まります。

以上より、コンテナクエリはコンポーネント指向で設計するプロジェクトに最適です。一方で、スタイルがコンポーネント単位で完結しないような要素に指定すると、かえって複雑性が増してしまうかもしれません。

メディアクエリでは、同じようなスタイルのコンポーネントでも、使用する領域のサイズに応じてスタイルを調整するケースがありました。

例えば、あるコンポーネントで広い領域に配置するときには、.--large のようなモディファイアを付与して対応することがあります。

コンテナクエリでは、そのときのコンポーネントのサイズによってスタイルが決まります。そのため、モディファイアのような個別の調整を必要としないため、再利用性が高まります。

メディアクエリとコンテナクエリの比較の図。メディアクエリでは、使用箇所に応じてセレクタやスタイルを調整するケースがあるが、コンテナクエリでは、コンテナ単位でスタイルを切り替えるため調整用のモディファイアは不要

この強みは、特に CSS グリッドを使用したレイアウトで効力を発揮するでしょう。

メディアクエリでは、ブレイクポイントごとに用意されたデザインカンプに基づいてスタイルを指定することが一般的ですが、その中間サイズで補完しきれないとバランスの悪い見た目になります。

コンテナクエリでは、ビューポート幅ではなくコンテナの幅を起点にスタイルが切り替わるため、より柔軟でスムーズなスタイルの変化を付けることが可能になります。

メディアクエリとコンテナクエリの比較の図。メディアクエリでは、中間サイズでバランスの悪い見た目になることがあるが、コンテナクエリでは、そのときのコンポーネントのサイズによってスタイルが決まる

コンテナクエリの課題

見出し「コンテナクエリの課題」

コンテナクエリはこれまでの RWD の考え方とは異なる手法のため、現時点ではいくつかの課題や懸念点が挙げられます。

RWD を前提としたデザインでは、これまではブレイクポイントごとにデザインカンプを用意することが一般的でしたが、コンテナクエリを導入する場合には、以下の点を考える必要があります。

  • どの要素を基準コンテナにするか
  • どのサイズ(横幅)で切り替えるか
  • ビューポート幅で切り替えるパターンも含まれるか

Figma であればコンポーネントプロパティを使用することになると思いますが、前述したオーディオプレーヤーの例のように、プレーヤー全体と UI 部分といったように、複数の基準コンテナを持つ場合には構造が複雑になるため、デザイナーの負担が大きくなります。

Figma のスクリーンショット
Figma のコンポーネントプロパティで、合計 3 つのバリエーションを用意

コンテナクエリの使い方にもよりますが、デザインとコーディングの担当者が異なる場合、何かしらのガイドラインを用意するか、頻繁にコミュニケーションが取れる環境が求められそうです。

従来のメディアクエリによるスタイルの切り替えでは、コーディングがひととおり完了した時点で、ブレイクポイントのパターンごとに検証する方法が一般的でした。

しかし、コンテナクエリの場合には、基準コンテナごとにスタイルの切り替えが可能になるので、その検証をどのように実施すれば品質を確保できるのかという課題があります。

UI コンポーネントを管理するツールを導入しているのであれば、コンポーネントごとに検証することが可能ですが、その場合でも、周囲の要素との組み合わせや使用するレイアウトによって、バランスの調整が必要になるかもしれません。

2023/07/12 追記 検証用の Bookmarklet を作成しました。

同じ要素にコンテナクエリとメディアクエリの両方を指定することができますが、責任の所在がわからなくなるので、極力避けたほうがよいでしょう。

/* 🙅‍♂️ コンテナサイズとビューポートサイズを混在させない */
@container (inline-size >= 600px) {
  .elem {
    padding: 24px;
  }
}
@media (width >= 1440px) {
  .elem {
    padding: 40px;
  }
}

同じ要素に対する指定でなければ、スタイルシート内でコンテナサイズとビューポートサイズの指定が同居するケースも出てくると思いますが、何かしらの指針がなければ、デザインの一貫性やコードの可読性が損なわれる可能性があります。

なお、ビューポートサイズ以外のメディアクエリであれば、混在しても問題ありません。

@container (inline-size >= 600px) {
  .elem {
    padding: 24px;
  }
}
/* 🙆‍♂️ ビューポートサイズ以外のメディアクエリであれば問題なし */
@media (any-hover: hover) {
  .elem:hover {
    background-color: var(--hover-color);
  }
}

Containment(サイズの封じ込め)

見出し「Containment(サイズの封じ込め)」

前述したように、container-type を指定すると、インライン軸またはブロック軸のサイズが封じ込めの対象となります。

  • container-type: size では、横軸・縦軸ともに対象となる
  • container-type: inline-size では、横軸が対象となる(書字方向が横書きの場合)

周囲の要素とのレイアウトから独立することになるため、組み合わせによってはレイアウト崩れが発生します。

以下の例では、画像要素を子孫に持つ基準コンテナ.cover-container を、親要素の display: flex の指定で他の要素と横並びにします。

このとき、基準コンテナに明示的に横幅を指定していないと、隣り合う要素と重なってしまいます。これは container: inline-size で、インライン軸のサイズが無視されてしまうためです。

アルバムカバー

LOREM

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

基準コンテナと隣り合う要素が重なる例
<section class="info">
  <div class="cover-container">
    <figure class="cover">
      <!-- 画像 -->
    </figure>
  </div>
  <div class="review">
    <h2 class="title">LOREM</h2>
    <p class="desc">
      Lorem ipsum dolor sit amet...
    </p>
  </div>
</section>

<style>
.info {
  display: flex;
}
.cover-container {
  container: cover / inline-size;
}
</style>

上記の例で「コンテナにサイズを指定」のチェックボックスをオンにすると、以下のスタイルが適用されるため、コンテナの領域が確保されます。

.cover-container {
  container: cover / inline-size;
  min-inline-size: 100px;
}

なお、具体的なサイズが決まっていないときには、inline-size: 100%width: 100% を指定すれば、このような重なりを回避するこができますが、もちろん銀の弾丸ではありません。

コンテナクエリのためだけに基準コンテナの要素(<div> など)を追加する場合には、DOM の構造が深くなります。これは、パフォーマンス面やコードの可読性に影響を及ぼします。

window.matchMedia に相当するメソッドがない

この見出しのリンク

2023/06/30 追記

JS でメディアクエリを参照するには、window.matchMedia を使用しますが、現状、コンテナクエリにはこれに相当するメソッドが存在しません。

@container において、px のような JS で検知できる単位で指定されていれば、ResizeObserver などを使用して対応できることもありますが、emch といった相対的な長さの単位で指定しているケースでは対応方法が見当たりません。

この問題は W3C の GitHub Issue でも議論されています。

レスポンシブイメージに対応していない

見出し「レスポンシブイメージに対応していない」

2023/10/14 追記

<img> 要素の sizes 属性や、<source> 要素の media 属性によって、メディアクエリによる画像ファイルの出し分けができますが、コンテナクエリには対応していません。

もし、実現できたとしても、基準となるコンテナ要素(親要素)とコンテナクエリを指定している子要素の両方に sizes 属性を指定する必要があり、非常に複雑でメンテナンス性が悪くなります。

こちらも W3C の GitHub Issue でも議論されていますが、簡単に答えは出ないようです。

スタッキングコンテキストが形成される

見出し「スタッキングコンテキストが形成される」

2024/06/22 追記

ほとんどの場合は悪影響を及ぼすことはありませんが、container-type を指定するとスタッキングコンテキストが形成される点にも注意が必要です。

スタッキングコンテキストは要素の重なり順(Z軸)の基準となる概念です。position: relative もしくは position: absolutez-index の組み合わせが代表的な例ですが、そのほかにもさまざまなスタイル指定によって形成されます。

背景要素に z-index: 1、重なっている正方形の要素に z-index: 10 を指定する例で考えてみます。HTML の構造と重なり順に関する CSS は以下のとおりです。

<div class="box">
  <!-- 背景要素 -->
  <div class="box-bg">z-index: 1</div>

  <!-- 正方形の親要素 -->
  <div class="square-parent">
    <!-- 正方形 -->
    <div class="square">z-index: 10</div>
  </div>
</div>

<style>
.box {
  position: relative;
}
.box-bg {
  position: absolute;
  z-index: 1;
}
.square {
  position: relative;
  z-index: 10;
}
</style>

以下はライブデモです。

container-type: inline-size のチェックボックスを有効にすると、正方形を囲っている要素(.parent)にその指定が適用され、見た目上の重なり順が変わります。

z-index: 1
z-index: 10

スタッキングコンテキストの詳細については、以下の記事をご参照ください。


これらに加えて、前提で述べたように、現時点では草案(Working Draft)段階のため、今後、実装方法や機能が変更される可能性がある点も留意しておいたほうがよいかもしれません。

このように、コンテナクエリには明確な特長がある反面、課題のほうが目立つ状態であるので、実際の案件で使用するには、まだまだリスクが大きいように感じます。

とはいえ、それで一蹴するのではなく、影響の少ないところから少しずつ取り入れて、使い方を探っていきたいと考えています。

コンテナクエリについては、以下の記事でも取り上げています。

本記事の作成にあたり、以下のウェブページを参考にしました。

脚注

  1. Responsive Web Design | A List Apart

  2. Container queries land in stable browsers | web.dev