- CSS
CSSコンテナクエリ|@containerの書き方とメディアクエリとの違い
CSSコンテナクエリとは、画面幅ではなく「親要素(コンテナ)の大きさや状態」に応じてスタイルを切り替える仕組みです。 親に container-type: inline-size を付け、子孫に向けて @container (width >= 400px) { ... } と書くだけで使えます。同じカード部品を、広いメイン領域でも狭いサイドバーでも、置かれた場所に合わせて自動で組み替えられます。
この記事では、構文を1枚の早見表にまとめたうえで、カード・サイドバー・グリッド・流体フォントのコピペで使えるパターンを紹介します。各パターンは Playwright+Chromium 141 で、コンテナ幅を変えながら getComputedStyle の値(flex-direction・列数・font-size)を実際に測った結果を載せています。さらに、上位記事ではあまり触れられていないスタイルクエリ style() と scroll-state() の対応状況(MDN の互換性データ 8.1.4 で確認)と動作結果、よくあるつまずき(自分自身には効かない、size で高さが 0 になる)の再現結果もまとめました。
CSSコンテナクエリの早見表
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 横幅でクエリできるコンテナにする | container-type: inline-size; |
いちばんよく使う値。横幅だけを封じ込める |
| 幅と高さの両方でクエリする | container-type: size; |
高さも封じ込めるため、高さの指定が必要 |
| サイズクエリに使わない(既定値) | container-type: normal; |
スタイルクエリ・名前の参照だけに使える |
| スクロール状態でクエリする | container-type: scroll-state; |
scroll-state() 用(Chromium 系のみ) |
| コンテナに名前を付ける | container-name: sidebar; |
複数名も可:container-name: a b; |
| 名前と種類をまとめて書く | container: sidebar / inline-size; |
省略形。名前 / 種類 の順 |
| 幅が400px以上のとき | @container (width >= 400px) { } |
範囲構文。(min-width: 400px) と同じ意味 |
| 名前を指定して判定する | @container sidebar (width >= 400px) { } |
最も近い「その名前の」祖先を見る |
| 条件を組み合わせる | and / or / not |
@container (width > 400px) and (height > 300px) |
| 縦横比・向き | (aspect-ratio > 1) / (orientation: landscape) |
size コンテナで使う |
| カスタムプロパティで切り替える | @container style(--variant: featured) { } |
スタイルクエリ。現状はカスタムプロパティのみ |
| sticky が貼り付いたら | @container scroll-state(stuck: top) { } |
Chromium 133 以降 |
| コンテナ幅の1% | cqw |
container query width |
| コンテナ高さの1% | cqh |
size コンテナが必要 |
| インライン方向(横書きなら横幅)の1% | cqi |
横書きでは cqw と同じ値。書字方向に強い |
| ブロック方向の1% | cqb |
横書きでは cqh と同じ値 |
cqi/cqb の小さい方・大きい方 |
cqmin / cqmax |
正方形に近い部品のサイズ調整に |
ポイントは「親に container-type、子孫に @container」の2点セットです。@container で指定したスタイルは、コンテナ自身ではなくその中の要素に効きます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSコンテナクエリ|@containerの書き方とメディアクエリとの違い」
ブラウザ対応状況(2026年10月時点)
MDN の互換性データ(@mdn/browser-compat-data 8.1.4)を npm で取得し、主要機能の初対応バージョンを確認しました。
| 機能 | Chrome | Firefox | Safari |
|---|---|---|---|
サイズクエリ(@container・container-type・cq* 単位) |
105 | 110 | 16 |
スタイルクエリ(カスタムプロパティ style(--x: y)) |
111 | 151 | 18 |
スタイルクエリの範囲構文(style(--n >= 3)) |
142 | 151 | 未対応 |
scroll-state()(stuck / snapped / scrollable) |
133 | 未対応 | 未対応 |
scroll-state(scrolled: ...) |
144 | 未対応 | 未対応 |
条件なしの名前だけのクエリ(@container card { }) |
148 | 149 | 26.4 |
アンカー位置のクエリ(container-type: anchored) |
143 | 未対応 | 未対応 |
サイズクエリは主要ブラウザすべてで使える(Baseline: Widely available) 状態です。スタイルクエリのカスタムプロパティ版も Firefox 151 で揃いました。scroll-state() は Chromium 系のみなので、対応していないブラウザでも見た目が成り立つ「おまけの演出」として使うのがおすすめです。
基本の書き方:3ステップ
1. 親要素をコンテナにする
.card-wrap {
container-type: inline-size; /* 横幅でクエリできるようにする */
}
2. 子孫のスタイルを @container で切り替える
.card {
display: flex;
flex-direction: column; /* 狭いときは縦並び */
gap: 12px;
}
@container (width >= 400px) {
.card {
flex-direction: row; /* コンテナが400px以上なら横並び */
}
}
3. HTML で包む
<div class="card-wrap">
<article class="card">
<img src="item.jpg" alt="" width="120" height="80">
<div>
<h2 class="card__title">注文の受付を開始しました</h2>
<p>本文テキスト</p>
</div>
</article>
</div>
判定の対象は、セレクタに一致した要素から見て「最も近い祖先のコンテナ」 です。名前を付けていない @container は、container-type が inline-size か size の最も近い祖先を使います。
名前付きコンテナと省略形
コンテナが入れ子になると、どの祖先で判定するかが分かりにくくなります。名前を付けて明示すると読みやすくなります。
.sidebar,
.main {
container: area / inline-size; /* container-name: area; container-type: inline-size; と同じ */
}
@container area (width >= 480px) {
.profile { grid-template-columns: 120px 1fr; }
}
and / or / not の組み合わせ
/* 両方満たす */
@container (width > 400px) and (height > 300px) { }
/* どちらか */
@container (width < 300px) or (orientation: portrait) { }
/* 否定(1つのクエリに1回) */
@container not (width < 400px) { }
/* 従来の min-/max- 形式も使える */
@container (min-width: 400px) and (max-width: 799.98px) { }
範囲構文 (400px <= width < 800px) も使えます。 境界の重なりを気にせず書けるので、新しく書くなら範囲構文が読みやすいです。
コピペで使えるパターン集と実測結果
ここからのパターンは、すべて1つの HTML に入れて Playwright(1.63.0)+ Chromium 141.0.7390.37 で読み込み、コンテナの幅を 320 / 480 / 768 / 1024px に変えて getComputedStyle で値を取得しました。
パターン1:幅でレイアウトが変わるカード
.card-wrap { container-type: inline-size; }
.card { display: flex; flex-direction: column; gap: 12px; }
.card__title { font-size: 1rem; }
@container (width >= 400px) {
.card { flex-direction: row; }
}
@container (width >= 700px) {
.card__title { font-size: 1.5rem; }
}
パターン2:グリッドの列数をコンテナ幅で変える
.list-wrap { container-type: inline-size; }
.list { display: grid; gap: 16px; grid-template-columns: 1fr; }
@container (width >= 500px) {
.list { grid-template-columns: repeat(2, 1fr); }
}
@container (width >= 800px) {
.list { grid-template-columns: repeat(3, 1fr); }
}
パターン3:コンテナ単位で流れるように変わる見出し
.hero { container-type: inline-size; }
.hero__title {
font-size: clamp(1rem, 5cqi, 2.5rem); /* 最小16px・コンテナ幅の5%・最大40px */
}
実測結果(Chromium 141)
| コンテナ幅 | カード flex-direction |
タイトル font-size |
一覧 grid-template-columns |
見出し font-size(5cqi) |
|---|---|---|---|---|
| 320px | column | 16px | 280px | 16px |
| 480px | row | 16px | 440px | 24px |
| 768px | row | 24px | 356px 356px | 38.4px |
| 1024px | row | 24px | 317.328px 317.328px 317.344px | 40px |
※一覧は ul の既定の padding-left: 40px を含めて測ったため、コンテナとして判定されるのは .list-wrap の幅です(列の幅は ul の内側の幅から算出)。
読み取れるポイントは次のとおりです。
- カードは 400px を境に縦→横、700px を境に文字が 16px→24px に切り替わりました。
- 一覧は 1列 → 2列 → 3列 と、コンテナ幅だけで列数が変わりました。
5cqiは 480px で 24px(480×5%)、768px で 38.4px。1024px では 51.2px になる計算ですが、clamp()の上限 40px で止まりました。
パターン4:サイドバーとメインで同じ部品を切り替える
画面幅は同じでも、置き場所の幅によって部品の形が変わる例です。
.layout { display: grid; grid-template-columns: 240px 1fr; }
.sidebar,
.main { container: area / inline-size; }
.profile { display: grid; grid-template-columns: 1fr; }
@container area (width >= 480px) {
.profile { grid-template-columns: 120px 1fr; }
}
<div class="layout">
<aside class="sidebar"><div class="profile">…</div></aside>
<main class="main"><div class="profile">…</div></main>
</div>
実測結果(.layout 幅 1000px):
sidebar profile: 240px / main profile: 120px 640px
同じ .profile が、サイドバー(240px)では1列、メイン(760px)では2列になりました。メディアクエリでは画面幅が同じ以上、両方とも同じ形になるため、これがコンテナクエリならではの使い方です。
パターン5:カード一覧の中の各カードを、カード自身の幅で整える
グリッドの「セル」をコンテナにすると、列数がいくつになっても、各カードは自分のセル幅に合わせて形を変えます。
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
.grid > li { container-type: inline-size; } /* セル=コンテナ */
.item { display: grid; gap: 8px; }
.item__name { font-size: clamp(0.95rem, 6cqi, 1.25rem); }
@container (width >= 360px) {
.item { grid-template-columns: 96px 1fr; } /* 広いセルでは画像を左に */
}
親のグリッドは auto-fill で列数を決め、中身はセル幅で決めるという役割分担にすると、メディアクエリを1つも書かずに済みます(このパターンは構成のみの紹介で、計測は上のパターン1〜4で行っています)。
スタイルクエリ style() の書き方と実測
スタイルクエリは、祖先のカスタムプロパティの値で子孫のスタイルを切り替える機能です。サイズクエリと違い、container-type を付けなくても使えます(すべての要素が既定の normal でスタイルクエリのコンテナになるため)。
.item { --variant: normal; }
.item.is-featured { --variant: featured; }
.badge { color: rgb(0, 0, 0); }
@container style(--variant: featured) {
.badge { color: rgb(200, 0, 0); }
}
実測結果(Chromium 141):
style query featured: rgb(200, 0, 0) normal: rgb(0, 0, 0)
--variant: featured を持つ親の中のバッジだけが赤になりました。クラス名で書き分ける方法と比べて、「状態」をカスタムプロパティ1つで子孫に伝えられるのが利点です。
範囲構文は Chrome 142 から
数値で比較する範囲構文も仕様にあります。
.cols { --cols: 4; }
@container style(--cols >= 3) {
.cols-child { color: rgb(0, 128, 0); }
}
実測結果(Chromium 141):
style range --cols>=3: rgb(0, 0, 0)
Chromium 141 では範囲構文は一致しませんでした。 MDN の互換性データでは範囲構文の対応は Chrome 142・Firefox 151 からで、互換性表のとおりの結果です。今のところは style(--cols: 4) のような値の完全一致で書くか、範囲構文はおまけ扱いにすると安心です。
| スタイルクエリの書き方 | 意味 | 備考 |
|---|---|---|
style(--theme: dark) |
計算値が dark と一致 |
未登録のプロパティはトークンが完全一致したとき |
style(--theme) |
プロパティが存在する | @property で登録済みなら初期値と異なるとき |
style(--n >= 3) |
数値の比較 | Chrome 142+ / Firefox 151+ |
style(color: red) |
通常のプロパティ | MDN によると現状どのブラウザも未対応 |
scroll-state() の書き方と実測
scroll-state() は、スクロールに関する状態(sticky が貼り付いたか、スクロールできる方向があるか、スナップしたか)で子孫のスタイルを切り替えるクエリです。コンテナに container-type: scroll-state を付けます。
sticky ヘッダーが貼り付いたら色を変える
.scroller { height: 100px; overflow: auto; }
.sticky {
position: sticky;
top: 0;
container-type: scroll-state; /* sticky 要素自身をコンテナにする */
}
.sticky__inner { background: rgb(255, 255, 255); }
@container scroll-state(stuck: top) {
.sticky__inner { background: rgb(0, 0, 255); }
}
上にスクロールできるときだけ「上へ」ボタンを出す
.scroller2 { height: 100px; overflow: auto; container-type: scroll-state; }
.to-top { opacity: 0; }
@container scroll-state(scrollable: top) {
.to-top { opacity: 1; }
}
実測結果(Chromium 141、scrollTop = 50 にした前後):
sticky before scroll: rgb(255, 255, 255)
to-top before scroll: 0
sticky after scroll: rgb(0, 0, 255)
to-top after scroll: 1
JavaScript のスクロールイベントや IntersectionObserver を書かずに、貼り付き状態とスクロール可能方向を CSS だけで判定できました。 対応していないブラウザでは @container scroll-state(...) のブロックが無視され、初期の見た目のままになります。
scroll-state() の条件 |
値の例 | 用途 |
|---|---|---|
stuck |
top / bottom / left / right / block-start など |
sticky 要素の影・色の切り替え |
scrollable |
top / bottom / x / y / block / inline など |
「上へ」ボタン、続きがある影 |
snapped |
x / y / block / inline / both |
スナップ中のスライドを強調 |
scrolled |
方向の値 | 直近のスクロール方向(Chrome 144+、今回の141では対象外) |
メディアクエリとの使い分け
| 観点 | メディアクエリ @media |
コンテナクエリ @container |
|---|---|---|
| 判定の基準 | ビューポート(画面)や端末の特性 | 祖先要素(コンテナ)の大きさ・スタイル・状態 |
| 準備 | 不要 | 親に container-type が必要(サイズ・scroll-state) |
| 向いている対象 | ページ全体の骨組み、ヘッダー・ナビの切り替え | カード・ウィジェットなど再利用する部品 |
| 同じ部品を別の幅の場所に置く | 場所ごとに書き分けが必要 | 1つの定義で自動対応 |
| 印刷・ダークモード・動きを減らす設定 | 判定できる(print、prefers-color-scheme など) |
判定できない |
| 単位 | vw / vh / dvh など |
cqi / cqb / cqw / cqh / cqmin / cqmax |
| コンテナ自身のスタイル | 変えられる | 変えられない(子孫のみ) |
判断の目安は「ページの骨組みはメディアクエリ、骨組みの中に置く部品はコンテナクエリ」 です。両方を組み合わせるのが一般的で、どちらか一方に置き換えるものではありません。
flowchart TD
A[切り替えたいもの] --> B{何に応じて変える?}
B -->|画面幅・端末の設定| C["@media"]
B -->|置かれた場所の幅| D["@container + container-type: inline-size"]
B -->|親から渡したい状態| E["@container style(--x: y)"]
B -->|sticky・スクロール状態| F["@container scroll-state()"]
つまずきやすいポイントと再現結果
1. container-type を付けた要素自身には効かない
.self { container-type: inline-size; width: 600px; color: rgb(0, 0, 0); }
@container (width >= 400px) {
.self { color: rgb(255, 0, 0); } /* 自分自身 */
.self > span { color: rgb(255, 0, 0); } /* 子要素 */
}
実測結果:
self color: rgb(0, 0, 0) child color: rgb(255, 0, 0)
コンテナ自身は黒のまま、子要素だけが赤になりました。@container は「その要素の祖先にあるコンテナ」を探すため、自分自身は判定の対象になりません。直し方は、コンテナ用のラッパー要素を1つ外側に足すことです。
<!-- 直す前 -->
<article class="card" style="container-type: inline-size">…</article>
<!-- 直した後:外側にコンテナを置く -->
<div class="card-wrap" style="container-type: inline-size">
<article class="card">…</article>
</div>
2. container-type: size にすると高さが 0 になる
.size-box { container-type: size; } /* 高さ指定なし */
.size-box-fixed { container-type: size; height: 200px; } /* 高さ指定あり */
実測結果(どちらも中に <p> あり):
size-box height: 0 fixed: 200
size は幅と高さの両方を封じ込めるため、中身の高さが親に伝わらず 0px になりました。 高さでクエリしたい場合は height や aspect-ratio を明示し、横幅だけで十分なら inline-size を選ぶとよいです。
3. inline-block や fit-content の要素に inline-size を付けると幅が 0 になる
<span class="ib" style="display:inline-block; container-type:inline-size">インラインブロック</span>
<span style="display:inline-block">インラインブロック</span>
実測結果:
inline-size + inline-block width: 0 normal: 147
inline-size は中身から横幅を決めることを止めるため、中身の幅に合わせる要素(inline-block、width: fit-content、flex の子で幅を指定しないもの など)は幅が 0 になります。 コンテナには、display: block のように親から幅をもらう要素を使うのが基本です。flex の子をコンテナにするときは flex: 1 や width を指定します。
4. コンテナがないときの cq 単位はビューポート基準になる
.nocq { font-size: 10cqw; } /* 祖先にコンテナなし */
実測結果(ビューポート幅 1024px):
no container 10cqw font-size at vw1024: 102.4px
祖先にコンテナがない場合、cq* 単位はスモールビューポート(svw 等)と同じ基準で計算されます。 意図せず大きな値になったときは、祖先に container-type が付いているかを確認します。
つまずきチェックリスト
| 症状 | 確認すること | 直し方 |
|---|---|---|
| @container がまったく効かない | 祖先に container-type があるか |
親(ラッパー)に inline-size を付ける |
| コンテナ自身の見た目が変わらない | 自分自身を対象にしていないか | 外側にラッパーを追加 |
| 要素の高さが 0 になった | container-type: size にしていないか |
inline-size に変えるか高さを指定 |
| 要素の幅が 0 になった | inline-block・fit-content・flex の子か | display: block や幅指定を加える |
| 名前付きクエリが効かない | container-name の綴り、名前の付いた祖先があるか |
container: 名前 / inline-size で揃える |
| 範囲構文のスタイルクエリが効かない | ブラウザのバージョン | 値の完全一致 style(--x: y) で書く |
cqw が大きすぎる |
コンテナが見つかっているか | 祖先にコンテナを追加 |
DevTools での確認方法
Chrome の DevTools では、container-type を持つ要素の横に container バッジが表示され、クリックするとコンテナの範囲がハイライトされます。Styles パネルの @container ルールにカーソルを合わせると、判定に使われたコンテナの要素と大きさが表示されるので、どの祖先で判定されているかを目で確認できます。
よくある質問
Q. container-type は inline-size と size のどちらを使えばよいですか?
A. ほとんどの場合は inline-size で十分です。 高さでクエリしたいときだけ size を使い、その際はコンテナの高さを height や aspect-ratio で明示します。
Q. メディアクエリはもう使わなくてよいですか?
A. いいえ、役割が違います。ページ全体のレイアウト、印刷、prefers-color-scheme などの利用者設定はメディアクエリで判定します。部品の中身の組み替えはコンテナクエリ、と分担すると整理しやすくなります。
Q. 対応していないブラウザではどうなりますか?
A. 対応していない @container ブロックは無視され、ブロックの外に書いた基本スタイルが使われます。基本スタイルを「狭い状態の見た目」で書いておき、広いときの差分を @container に書くと、どの環境でも崩れにくくなります。
Q. コンテナクエリを使うとパフォーマンスは下がりますか?
A. container-type は封じ込め(containment)によってレイアウト計算の範囲を区切るための指定で、仕組みとして計算範囲を限定します。ただし多数の要素に付ける場合は、DevTools の Performance パネルで実際の描画時間を確認しながら、必要な部品に絞って付けるとよいです。
まとめ
- 親に
container-type: inline-size、子孫に@container (width >= 400px)が基本の2点セット container: 名前 / inline-sizeの省略形と名前付きクエリで、入れ子でも判定先が明確になるcqiとclamp()の組み合わせで、コンテナ幅に合わせて流れる文字サイズが作れる(実測:480px で 24px、上限 40px)- スタイルクエリ
style(--x: y)は主要ブラウザで利用可能。範囲構文は Chrome 142・Firefox 151 から scroll-state()は Chromium 系のみ。sticky の貼り付きやスクロール可能方向を CSS だけで判定できる- つまずきは4つ:自分自身には効かない/
sizeで高さ 0/inline-block で幅 0/コンテナなしのcq*はビューポート基準 - ページの骨組みはメディアクエリ、部品はコンテナクエリと使い分ける
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSコンテナクエリ|@containerの書き方とメディアクエリとの違い」
株式会社RJCでは、CSSの新しいレイアウト機能を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。