同じカードのデザインでも、メイン領域に置けば画像とテキストを横に並べたい、サイドバーに置けば縦に積みたい、ということはよくあります。これをメディアクエリでやろうとすると「画面が広いときはサイドバーのカードも横並びになってしまう」という問題にぶつかります。@container(コンテナクエリ)は、画面幅ではなく親要素の幅を条件にスタイルを切り替えるための仕組みです。この記事では基本の書き方から container-type の値、名前付きコンテナ、コンテナ幅を基準にする単位までを、動かせるデモつきで解説します。
目次
メディアクエリとコンテナクエリの違い
これまでレスポンシブ対応といえば @media、つまりメディアクエリでした。メディアクエリが見ているのはビューポート(画面)の幅です。「画面が 768px 以上なら横並び」という書き方はページ全体のレイアウトを切り替えるのには向いていますが、ページのどこに置かれているかまでは考慮してくれません。
一方コンテナクエリが見るのはその要素を囲んでいる親要素(コンテナ)の幅です。画面が 1200px あっても、カードが 240px のサイドバーに入っているなら「狭いときのスタイル」が適用されます。判断の基準がコンポーネントの置かれた場所そのものになるので、同じマークアップのカードをメイン領域にもサイドバーにも、記事下のウィジェットにも置けるようになります。コンポーネント単位で CSS を書けるようになる、というのがいちばんの利点です。
コンテナクエリは 2023 年に主要なブラウザへ出そろった機能で、Chrome・Edge・Safari・Firefox のいずれの現行版でも利用できます。
コンテナクエリは2ステップで書く
書き方はとても単純で、やることは2つだけです。まず基準にしたい親要素に container-type を指定して「コンテナ」だと宣言し、次にその中の要素に対するスタイルを @container で囲むだけです。
/* 1. 親要素をコンテナにする */
.card-area {
container-type: inline-size;
}
/* 2. コンテナの幅が 400px 以上のときのスタイルを書く */
@container (min-width: 400px) {
.card {
display: flex;
}
}
ここで min-width: 400px が指すのは .card-area の幅であって、画面幅ではありません。条件の書き方(min-width / max-width、and や not による組み合わせ)はメディアクエリとほぼ同じ感覚で書けます。
実際に動かしてみます。次のデモには同じマークアップのカードが2つあり、上は幅いっぱいの枠、下は幅 220px の枠に入っています。CSS も HTML も1組しか書いていませんが、入っている枠の幅が違うだけで見た目が変わります。
<div class="area area--wide">
<p class="area__label">幅の広い枠(コンテナ)</p>
<div class="card">
<h2 class="card__title">カードコンポーネント</h2>
<p class="card__state"></p>
</div>
</div>
<div class="area area--narrow">
<p class="area__label">幅 220px の枠(コンテナ)</p>
<div class="card">
<h2 class="card__title">カードコンポーネント</h2>
<p class="card__state"></p>
</div>
</div>
body {
margin: 16px;
font-family: sans-serif;
color: #333;
}
/* 1. 親要素をコンテナにする */
.area {
container-type: inline-size;
margin-bottom: 20px;
padding: 12px;
border: 1px dashed #adb5bd;
border-radius: 6px;
}
.area--wide {
width: 100%;
}
.area--narrow {
width: 220px;
}
.area__label {
margin: 0 0 8px;
color: #868e96;
font-size: 12px;
}
.card {
padding: 12px 16px;
border: 1px solid #dee2e6;
border-radius: 6px;
background: #f8f9fa;
}
.card__title {
margin: 0 0 4px;
font-size: 15px;
}
.card__state {
margin: 0;
font-size: 13px;
}
.card__state::before {
content: "コンテナ幅 320px 未満のスタイル";
}
/* 2. 子要素をコンテナの幅で切り替える */
@container (min-width: 320px) {
.card {
border-color: #007bff;
background: #e7f1ff;
}
.card__title {
color: #0a58ca;
font-size: 20px;
}
.card__state::before {
content: "コンテナ幅 320px 以上のスタイル";
}
}
プレビューの幅を変えても、下のカードは 220px の枠に入ったままなので見た目は変わりません。画面ではなく枠の幅で判断していることが分かると思います。
container-type に指定できる3つの値
container-type は「その要素のどのサイズを問い合わせられるようにするか」を決めるプロパティです。指定できる値は次の3つです。
| 値 | 効果 |
|---|---|
normal | 初期値。サイズを問い合わせるコンテナにはならない(後述のスタイルクエリの対象にはなる) |
inline-size | インライン方向のサイズ、横書きなら幅だけを問い合わせられる。ふだん使うのはほぼこれ |
size | 幅と高さの両方を問い合わせられる。高さ方向も封じ込められるため、親に明示的な高さの指定が必要 |
ほとんどの場面では inline-size を選んでおけば問題ありません。size にすると要素の高さが「中身の量」から決まらなくなり、高さを指定していない要素は潰れて 0 になってしまいます。height や aspect-ratio であらかじめ高さが決まっている枠でなければ、レイアウトが崩れる原因になります。
normal にしたときの「スタイルクエリ」というのは、@container style(--theme: dark) のようにコンテナのカスタムプロパティの値を条件にする書き方のことです。サイズとは別の仕組みなので、container-type を指定していない要素でも親として機能します。
container-name で狙うコンテナを指定する
名前を付けずに @container (min-width: …) と書いた場合、条件が照合されるのはその要素からもっとも近い祖先のコンテナです。コンテナが入れ子になっていると、内側のコンテナが優先されて「外側の枠の幅で切り替えたかったのに効かない」ということが起こります。
そこで container-name でコンテナに名前を付けておき、@container の直後にその名前を書くと、離れた祖先のコンテナも名指しできます。container-type と container-name はショートハンドの container でまとめて書けます。順番は名前・スラッシュ・種類です。
.card-area {
/* 個別に書く場合 */
container-type: inline-size;
container-name: card;
}
.card-area {
/* ショートハンドなら1行(名前 / 種類 の順) */
container: card / inline-size;
}
/* card という名前のコンテナだけを見る */
@container card (min-width: 400px) {
.card {
display: flex;
}
}
名前は自由に決められますが、none のような CSS の予約語は使えません。スペース区切りで container-name: card sidebar; のように複数の名前を持たせることもでき、その場合はどちらの名前でも狙えます。
次のデモでは、幅いっぱいの外側の枠を layout、その中にある幅 220px の枠を panel という名前のコンテナにしています。3行のテキストはそれぞれ「名前なし」「panel を名指し」「layout を名指し」の条件で書かれていて、条件に一致した行だけ緑色になります。
<div class="layout">
<p class="label">layout(幅いっぱいのコンテナ)</p>
<div class="panel">
<p class="label">panel(幅 220px のコンテナ)</p>
<p class="line line--nearest">@container (min-width: 320px)</p>
<p class="line line--panel">@container panel (min-width: 320px)</p>
<p class="line line--layout">@container layout (min-width: 320px)</p>
</div>
</div>
body {
margin: 16px;
font-family: sans-serif;
color: #333;
}
/* 外側のコンテナに layout という名前を付ける */
.layout {
container: layout / inline-size;
padding: 12px;
border: 1px dashed #adb5bd;
border-radius: 6px;
}
/* 内側のコンテナに panel という名前を付ける */
.panel {
container: panel / inline-size;
width: 220px;
padding: 12px;
border: 1px dashed #adb5bd;
border-radius: 6px;
}
.label {
margin: 0 0 8px;
color: #868e96;
font-size: 12px;
}
.line {
margin: 0 0 6px;
padding: 6px 8px;
border-radius: 4px;
background: #f1f3f5;
color: #868e96;
font-family: monospace;
font-size: 12px;
}
.line::after {
content: " → 一致しない";
}
/* 名前なし:もっとも近いコンテナ(panel)の幅を見る */
@container (min-width: 320px) {
.line--nearest {
background: #d3f9d8;
color: #2b8a3e;
}
.line--nearest::after {
content: " → 一致する";
}
}
/* panel を名指しで見る */
@container panel (min-width: 320px) {
.line--panel {
background: #d3f9d8;
color: #2b8a3e;
}
.line--panel::after {
content: " → 一致する";
}
}
/* layout を名指しで見る */
@container layout (min-width: 320px) {
.line--layout {
background: #d3f9d8;
color: #2b8a3e;
}
.line--layout::after {
content: " → 一致する";
}
}
名前なしの条件と panel を名指しした条件が同じ結果になっているのは、テキストからもっとも近い祖先のコンテナが panel だからです。外側の layout は 320px 以上の幅がありますが、名指ししない限り見てもらえません。入れ子が深くなるほどこの取り違えは起きやすいので、コンポーネントの外枠には名前を付けておくと安全です。
もうひとつ覚えておきたいのが、@container の中で書いたスタイルはコンテナ自身には当たらないということです。条件を照合する相手であるコンテナは、その時点ですでにレイアウトが確定している必要があるため、自分自身のサイズを条件に自分のスタイルを変えることはできません。切り替えたいスタイルは必ずコンテナの中身(子孫要素)に書きます。カード自体の見た目を変えたいなら、カードをもう1枚のラッパーで包み、そのラッパーをコンテナにするのが定石です。
コンテナの幅を基準にする cqi・cqw などの単位
コンテナクエリと同時に、コンテナのサイズを基準にした長さの単位も使えるようになっています。ビューポート基準の vw や vh のコンテナ版だと考えると分かりやすいはずです。
| 単位 | 意味 |
|---|---|
cqw | コンテナの幅の 1% |
cqh | コンテナの高さの 1% |
cqi | コンテナのインライン方向のサイズの 1%(横書きなら幅) |
cqb | コンテナのブロック方向のサイズの 1%(横書きなら高さ) |
cqmin | cqi と cqb のうち小さいほう |
cqmax | cqi と cqb のうち大きいほう |
たとえば font-size: 5cqi; と書けば、コンテナの幅の 5% の文字サイズになります。ただしそのまま使うと狭いコンテナで文字が読めないほど小さくなるため、clamp() で下限と上限を挟むのが実用的です。
.card-area {
container-type: inline-size;
}
.card__title {
/* 最小 15px、最大 22px の範囲でコンテナ幅の 5% */
font-size: clamp(15px, 5cqi, 22px);
}
注意したいのは高さ方向の単位です。container-type: inline-size のコンテナは高さを封じ込めていないため、cqh や cqb はそのコンテナを基準にできません。この場合は高さを問い合わせられる祖先のコンテナまでさかのぼり、それも無ければビューポートの高さが使われます。高さを基準にしたいときは container-type: size が必要になります。
同じカードをメイン領域とサイドバーに置いてみる
実際のレイアウトに近い形で試してみます。次のデモでは、幅いっぱいの「メイン領域」と幅 200px の「サイドバー」に、まったく同じマークアップのカードを置いています。カードの CSS は狭いとき(縦積み)をベースにして、コンテナが 280px 以上になったら横並びに切り替えています。タイトルの文字サイズには先ほどの cqi を使っています。
<div class="slot slot--main">
<article class="card">
<div class="card__thumb">IMAGE</div>
<div class="card__body">
<h2 class="card__title">記事のタイトルが入ります</h2>
<p class="card__text">メイン領域では画像とテキストが横並びになります。</p>
</div>
</article>
</div>
<div class="slot slot--side">
<article class="card">
<div class="card__thumb">IMAGE</div>
<div class="card__body">
<h2 class="card__title">記事のタイトルが入ります</h2>
<p class="card__text">サイドバーでは縦積みになります。</p>
</div>
</article>
</div>
body {
margin: 16px;
font-family: sans-serif;
color: #333;
}
/* カードを置く枠をコンテナにする */
.slot {
container-type: inline-size;
margin-bottom: 20px;
}
.slot--main {
width: 100%;
}
.slot--side {
width: 200px;
}
/* 狭いときの見た目(縦積み)をベースにする */
.card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
border: 1px solid #dee2e6;
border-radius: 8px;
background: #fff;
}
.card__thumb {
display: grid;
place-items: center;
aspect-ratio: 16 / 9;
border-radius: 6px;
background: #ced4da;
color: #fff;
font-size: 12px;
letter-spacing: 0.1em;
}
.card__title {
margin: 0 0 6px;
/* コンテナ幅の 5% を基準にしたフォントサイズ */
font-size: clamp(15px, 5cqi, 22px);
line-height: 1.4;
}
.card__text {
margin: 0;
color: #495057;
font-size: 13px;
}
/* コンテナが 280px 以上なら横並びに切り替える */
@container (min-width: 280px) {
.card {
flex-direction: row;
align-items: center;
}
.card__thumb {
flex: none;
width: 40%;
}
}
ポイントは、カード側の CSS に「サイドバー用」「メイン用」といったクラスがひとつも出てこないことです。従来なら .sidebar .card のように置き場所ごとの打ち消しを書くところですが、コンテナクエリではカードが自分の使える幅を見て勝手に形を変えるため、置く側は枠を用意するだけで済みます。あとから3カラムのレイアウトを追加しても、カードの CSS には手を入れずに済むわけです。
なお、狭いときのスタイルをベースにして min-width で広いときを上書きする書き方(モバイルファースト)にしておくと、条件に一致しなかったときのフォールバックが自然な形になります。コンテナクエリに対応していない古い環境でも、縦積みのカードとして読める状態が保たれます。
コンテナクエリが効かないときに見直すところ
書いたはずのスタイルが当たらない、あるいは当たったとたんにレイアウトが崩れる場合、原因はだいたい次の4つに絞られます。
親に container-type を指定し忘れている
もっとも多いのがこれです。@container は「コンテナ」として宣言された祖先要素がなければ、条件を照合する相手が見つからず、中のスタイルはまるごと無視されます。エラーも警告も出ないので気づきにくいところです。container-type: inline-size(または container ショートハンド)が、切り替えたい要素の祖先に書かれているかを確認してください。
ありがちなのは、コンテナにしたつもりの要素と実際に container-type を書いた要素がずれているケースです。ブラウザの開発者ツールで対象要素を選び、祖先をたどって container-type が適用されている要素があるかを見てみましょう。Chrome の開発者ツールでは、コンテナになっている要素に container のバッジが表示されます。
コンテナ自身にスタイルを当てようとしている
container-type を書いた要素と、@container の中でセレクターに指定した要素が同じになっていないでしょうか。前述のとおり、コンテナは自分自身のサイズを条件に自分のスタイルを変えられません。.card { container-type: inline-size; } と書いたうえで @container (min-width: 400px) { .card { display: flex; } } としても、何も起こらないということです。
解決策は、コンテナにする要素をひとつ外側に用意することです。.card-area > .card のようにラッパーで包み、.card-area をコンテナ、.card をスタイル変更の対象にします。Flexbox や Grid のアイテムをコンテナにする場合も同じで、アイテムそのものではなく、アイテムの中身にスタイルを当てます。
container-type: size にしたら高さが潰れた
container-type: size は幅と高さの両方を封じ込めるため、その要素の高さは中身の量から計算されなくなります。高さを指定していなければ 0 として扱われ、中身が下にはみ出したり、要素が見えなくなったりします。「高さも条件にしたい」という理由だけで size を選ぶと、たいていこの問題にぶつかります。
高さ方向の条件が本当に必要でないなら inline-size に戻すのが確実です。どうしても size が必要なときは、height や aspect-ratio、あるいは Grid のトラックなど、外側から高さが決まる文脈でだけ使うようにします。
コンテナにした要素の周りでレイアウトが変わった
container-type を normal 以外にすると、その要素にはレイアウトの封じ込めも適用されます。見た目に影響するのは主に3点で、中の要素とマージンの相殺が起こらなくなること、新しいスタッキングコンテキストが作られること、そして position: absolute や fixed の子要素にとってこの要素が位置の基準(包含ブロック)になることです。
コンテナ化した直後に上下の余白が変わったなら、マージンの相殺が止まったことが原因です。画面全体を覆うはずのオーバーレイがコンテナの内側に収まってしまったなら、包含ブロックが変わったせいです。いずれも container-type の副作用なので、コンテナにする要素はレイアウト上あってもなくても影響の小さいラッパーにしておくと、こうしたトラブルを避けられます。
まとめ
@container は、画面幅ではなく親要素の幅を条件にスタイルを切り替える仕組みです。使い方は、親に container-type: inline-size を指定してコンテナにし、子に対するスタイルを @container (min-width: …) で囲む、という2ステップだけです。入れ子になったコンテナを名指ししたいときは container: card / inline-size; のように名前を付け、@container card (…) で狙います。コンテナ自身にはスタイルが当たらないこと、size を選ぶと高さが潰れることの2点に気をつければ、置き場所に左右されないコンポーネントを素直に書けるようになります。