CSS Container Queries 完全ガイド|@container・size vs inline-size・コンポーネント単位レスポンシブ
Container Queries(@container) は、ビューポート幅ではなく親コンテナのサイズを基準にスタイルを切り替えるCSS仕様だ。2026年のフロントエンド実務では container-type: inline-size を親に付け、子で @container を書く のがベースライン。Media Query はページ骨格、Container Query はカード・パネル・ウィジェット——役割を分けると、Design System の再利用性が一気に上がる。
Container Queries とは — なぜ Media Query だけでは足りないのか
2010年代のレスポンシブWebは @media (min-width: 768px) を軸に設計された。スマホ・タブレット・PCという画面サイズの3段階で、ページ全体のグリッドやナビゲーションを切り替えるのは理にかなっていた。
しかし2020年代のUIはもっと複雑だ。
- ダッシュボード — サイドバーを開閉するとメインカラム幅だけが変わる。カードコンポーネントは「画面幅」ではなく「割り当てられた列幅」で見た目を決めたい
- CMSの埋め込みブロック — 同じ「記事カード」が1カラム記事・2カラムサイドバー・Related Posts グリッドに再利用される
- Design System — Storybook 上のプレビュー枠が 320px でも 1200px でも、コンポーネント単体で正しく崩れないことが求められる
Media Query は常にビューポート(またはルート要素)を参照する。親の が 280px しかなくても、ビューポートが 1440px なら の「PC向けスタイル」が当たってしまう——これが 「コンポーネントが親に合わせて伸縮しない」 問題の正体だ。
CSS Container Queries はこの問題を、コンポーネントの直近のコンテナ祖先のサイズに基づいてスタイルを適用することで解決する。仕様上のキーワードは次の3つだ。
| プロパティ / 構文 | 役割 |
|---|---|
container-type | 要素を「サイズクエリの対象コンテナ」として宣言する |
container-name | 名前付きコンテナとして @container sidebar のように参照する |
@container | コンテナサイズに応じた条件分岐(Media Query のコンテナ版) |
Container Queries Level 1 は 2022〜2023年に主要ブラウザへ実装され、2026年時点では新規プロジェクトのデフォルト選択肢になっている。
2026年ベースライン — ブラウザ対応と採用判断
2026年6月時点の実装状況を整理する。
| 環境 | Container Queries 対応 |
|---|---|
| Chrome / Edge | 105+(2022年9月〜)。DevTools に Container overlay あり |
| Firefox | 110+(2023年2月〜)。@container / container-type 完全対応 |
| Safari / iOS | 16.0+(2022年9月〜)。macOS Ventura 以降 |
| Samsung Internet | Chromium 系。Chrome と同等 |
| IE11 / レガシー WebView | 非対応。@supports フォールバック必須 |
採用の目安
- B2C のモダンブラウザ向けサービス → Container Queries を前提に設計して問題なし
- 社内ツールで Chrome 固定 → 即日導入可
- 政府・金融系で IE 互換要件 → Progressive Enhancement(後述のフォールバック節参照)
Can I use 上の「Baseline Widely available」に該当する機能として、2026年のフロントエンド求人・Design System ガイドでも @container は当たり前の語彙になっている。未対応ブラウザ向けの @media フォールバックは「保険」として残しつつ、主設計は Container Query 基準で進めるのが合理的だ。
※ 値は「コンポーネントの再利用性スコア(相対値・説明用)」として記載。実プロジェクトでは Design System の成熟度に依存する。
最小構成 — container-type と @container の基本
Container Query を動かすには 2ステップ だけ覚えればよい。
- 親(コンテナ) に
container-typeを指定する - 子(コンポーネント内) で
@containerルールを書く
基本例:横並び ↔ 縦積みカード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Container Query 基本例</title>
<style>
/* ページレイアウト — Media Query で骨格を決める */
.page {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
max-width: 1200px;
margin: 0 auto;
}
@media (min-width: 900px) {
.page {
grid-template-columns: 280px 1fr;
}
}
/* サイドバー列 — ここが「コンテナ」になる */
.sidebar-slot {
container-type: inline-size;
container-name: sidebar;
}
/* カードコンポーネント — ビューポートではなく sidebar 幅を見る */
.profile-card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #ffffff;
}
.profile-card__avatar {
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.profile-card__body {
flex: 1;
}
.profile-card__name {
font-size: 1.125rem;
font-weight: 700;
margin: 0 0 0.25rem;
}
.profile-card__bio {
font-size: 0.875rem;
color: #64748b;
margin: 0;
line-height: 1.5;
}
/* コンテナ幅が 360px 以上なら横並びレイアウトへ */
@container sidebar (min-width: 360px) {
.profile-card {
flex-direction: row;
align-items: center;
}
.profile-card__avatar {
width: 80px;
height: 80px;
}
}
/* メインカラム用の別コンテナ */
.main-slot {
container-type: inline-size;
container-name: main;
}
@container main (min-width: 480px) {
.profile-card {
flex-direction: row;
align-items: center;
}
}
</style>
</head>
<body>
<div class="page">
<aside class="sidebar-slot">
<article class="profile-card">
<div class="profile-card__avatar" aria-hidden="true"></div>
<div class="profile-card__body">
<h2 class="profile-card__name">山田 太郎</h2>
<p class="profile-card__bio">フロントエンドエンジニア。Container Queries でコンポーネント設計を研究中。</p>
</div>
</article>
</aside>
<main class="main-slot">
<article class="profile-card">
<div class="profile-card__avatar" aria-hidden="true"></div>
<div class="profile-card__body">
<h2 class="profile-card__name">同じ .profile-card クラス</h2>
<p class="profile-card__bio">メインカラムでは幅に余裕があるため、@container main の閾値で横並びになる。</p>
</div>
</article>
</main>
</div>
</body>
</html>
ポイントは 同じ .profile-card クラスが、親コンテナの幅に応じて異なる見た目になる ことだ。ビューポートが 1440px でも、サイドバー内カードは縦積みのまま——Media Query だけでは実現できなかった挙動だ。
単一の @container だけ使うなら container-name は不要だ。1ページ内に複数種類のコンテナ(sidebar / main / widget)があり、同じ子コンポーネントがどのコンテナのクエリに反応するかを限定したいときに名前を付ける。
container-type: inline-size vs size — 使い分けの核心
container-type には主に inline-size と size がある(normal はクエリ対象外)。ここを誤ると「クエリが効かない」「レイアウトが崩れる」「パフォーマンスが悪化する」のいずれかが起きる。
| container-type | 特徴と向いている用途 |
|---|---|
| inline-size | インライン方向(横書きなら幅)のみ追跡。高さは子の内容に委ねる。大半のUIコンポーネント向け。再計算コスト低 |
| size | 幅と高さの両方を追跡。aspect-ratio 連動、画像トリミング切替など高さ依存レイアウト向け。子の % height が解決しやすくなる代わりコスト高 |
| normal(デフォルト) | サイズクエリ非対応。@container はこの祖先をスキップする |
inline-size がデフォルト選択である理由
横書き(writing-mode: horizontal-tb)の日本語Webページでは、インライン方向 = 水平方向 = 幅 だ。カードの「1列 ↔ 2列」「アイコンを上 ↔ 横」といった典型的なレスポンシブは幅の閾値だけで足りる。
は CSS Containment の inline-size containment を有効にする。コンテナの高さは子要素の intrinsic height に依存するため、親子間の循環依存(サイズの鶏と卵)が起きにくい。Layout エンジンにとっても、再計算範囲が限定されやすい。
size が必要になるケース
次のような要件では size を検討する。
- コンテナの高さが固定(
height: 300px)で、その中で画像のobject-fit: cover↔containを切り替えたい - 正方形に近いタイルで、幅と高さの両方が閾値を超えたら詳細テキストを表示する
- チャートやマップのオーバーレイで、パネルの縦横比に応じて凡例の位置を変える
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>container-type: size の例</title>
<style>
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
padding: 1rem;
}
.tile {
container-type: size;
container-name: tile;
height: 220px;
border-radius: 16px;
overflow: hidden;
position: relative;
background: #0f172a;
}
.tile__image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.tile__caption {
position: absolute;
inset: auto 0 0 0;
padding: 0.75rem 1rem;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
color: #fff;
font-size: 0.875rem;
transform: translateY(100%);
transition: transform 0.2s ease;
}
/* 幅も高さも十分なタイルだけキャプション常時表示 */
@container tile (min-width: 240px) and (min-height: 200px) {
.tile__caption {
transform: translateY(0);
}
.tile__image {
object-fit: cover;
}
}
/* 小さいタイルは contain で全体を見せる */
@container tile (max-width: 239px), (max-height: 199px) {
.tile__image {
object-fit: contain;
background: #1e293b;
}
}
</style>
</head>
<body>
<div class="gallery">
<figure class="tile">
<img class="tile__image" src="/images/sample-a.webp" alt="サンプル画像 A" width="400" height="300">
<figcaption class="tile__caption">高解像度タイル — キャプション常時表示</figcaption>
</figure>
<figure class="tile" style="height: 160px;">
<img class="tile__image" src="/images/sample-b.webp" alt="サンプル画像 B" width="400" height="300">
<figcaption class="tile__caption">低いタイル — object-fit: contain</figcaption>
</figure>
</div>
</body>
</html>
container-type: size をページ全体の <main> に付けると、子孫すべてのレイアウト計算がコンテナサイズ解決に縛られ、Layout イベントが増える可能性がある。Above-the-fold の広い領域より、個別タイル・パネル単位に閉じ込めるのが安全だ。計測の考え方は クリティカルレンダリングパス最適化 を参照。
@container 構文 — 条件式と Size Query の書き方
Container Query Level 1 では、Media Query と似た Size Query 構文が使える。
/* 基本形 */
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 120px 1fr;
gap: 1rem;
}
}
/* 名前付きコンテナ */
@container sidebar (min-width: 280px) {
.nav {
flex-direction: row;
gap: 0.5rem;
}
}
/* 複数条件 */
@container (min-width: 320px) and (max-width: 599px) {
.card {
padding: 1rem;
font-size: 0.9375rem;
}
}
/* 範囲構文(新しい記法) */
@container (400px <= width <= 799px) {
.card {
flex-direction: row;
align-items: center;
}
}
/* 高さ条件 — container-type: size が必要 */
@container (min-height: 300px) {
.panel {
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
/* aspect-ratio */
@container (aspect-ratio > 3/2) {
.hero {
flex-direction: row;
align-items: center;
}
}
クエリ可能な軸
| 軸 | 必要な container-type | 例 |
|---|---|---|
width / inline-size | inline-size または size | (min-width: 480px) |
height / block-size | size のみ | (min-height: 200px) |
aspect-ratio | size | (aspect-ratio > 1/1) |
orientation | size | (orientation: landscape) |
inline-size だけのコンテナで min-height を指定してもマッチしない。エディタの lint や DevTools が警告を出さないこともあるため、height 条件を書く前に container-type を確認する習慣が大切だ。
コンテナクエリ単位 — cqw, cqi, cqb, cqmin, cqmax
Media Query 時代の vw / vh はビューポート基準だ。Container Query では コンテナ基準の長さ単位 が使える。
| 単位 | 意味 |
|---|---|
cqw | コンテナ幅の 1% |
cqh | コンテナ高さの 1% |
cqi | コンテナインラインサイズの 1% |
cqb | コンテナブロックサイズの 1% |
cqmin | cqi と cqb の小さい方 |
cqmax | cqi と cqb の大きい方 |
Fluid Typography をコンテナ基準で
.article-card {
container-type: inline-size;
}
.article-card__title {
/* コンテナ幅 280px で約 1.25rem、560px で約 1.75rem */
font-size: clamp(1.25rem, 0.5rem + 4cqi, 1.75rem);
}
.article-card__excerpt {
font-size: clamp(0.875rem, 0.75rem + 1cqi, 1rem);
line-height: 1.6;
}
@container (min-width: 520px) {
.article-card__meta {
display: flex;
gap: 1rem;
}
}
vw ベースの Fluid Typography は、サイドバー内の狭いカードでも「画面がデスクトップだから大きい文字」になってしまう。cqi + clamp() はコンポーネントの実占有幅に追従するため、Design System のタイポグラフィトークンをコンテキスト非依存に保てる。
アクセシビリティのため、cqi だけで fontsize を決めず clamp(1rem, 0.5rem + 3cqi, 1.5rem) のように rem を下限に含める。ユーザー拡大(ブラウザズーム)との兼ね合いは Fluid Typography と同じ原則が適用される。
Media Query との役割分担 — ページ vs コンポーネント
Container Queries は Media Queries を置き換えるものではない。レイヤーを分ける。
| Media Query (@media) | Container Query (@container) |
|---|---|
| 参照対象 | ビューポート / ユーザー環境 |
| 参照対象 | 直近の名前付き・無名コンテナ祖先 |
| 向いている用途 | グリッド列数、固定ヘッダー、print、prefers-reduced-motion |
| 向いている用途 | カードレイアウト、ナビ折りたたみ、テーブル→カード変換 |
| Design System | ページテンプレート・ルート変数 |
| Design System | Button / Card / DataTable 等の再利用コンポーネント |
推奨パターン:外側 Media、内側 Container
/* ページ骨格 — Media Query */
.app-shell {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 1024px) {
.app-shell {
grid-template-columns: 240px 1fr 320px;
}
}
/* 各カラムスロット — Container */
.app-shell__aside,
.app-shell__main,
.app-shell__panel {
container-type: inline-size;
}
/* コンポーネント — Container Query のみ */
@container (min-width: 400px) {
.data-table {
display: table;
}
}
@container (max-width: 399px) {
.data-table {
display: block;
}
.data-table tr {
display: flex;
flex-direction: column;
border: 1px solid #e2e8f0;
margin-bottom: 0.75rem;
border-radius: 8px;
padding: 0.75rem;
}
}
ページが 3カラムになっても、右パネル(320px)内の DataTable はコンテナ幅が狭いためカード表示のまま——これがコンポーネントレベルレスポンシブの理想形だ。
実践例1 — 商品カードグリッド(eコマース)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>商品カード — Container Queries</title>
<style>
:root {
--color-border: #e5e7eb;
--color-muted: #6b7280;
--color-accent: #2563eb;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1rem;
padding: 1rem;
}
.product-card {
container-type: inline-size;
container-name: product;
}
.product-card__inner {
display: flex;
flex-direction: column;
height: 100%;
border: 1px solid var(--color-border);
border-radius: 12px;
overflow: hidden;
background: #fff;
}
.product-card__media {
aspect-ratio: 4 / 3;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-muted);
font-size: 0.75rem;
}
.product-card__content {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
}
.product-card__title {
font-size: clamp(0.875rem, 0.5rem + 2.5cqi, 1rem);
font-weight: 600;
margin: 0;
line-height: 1.4;
}
.product-card__price {
font-size: 1.125rem;
font-weight: 700;
color: var(--color-accent);
margin: 0;
}
.product-card__actions {
margin-top: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.product-card__button {
padding: 0.5rem 0.75rem;
border-radius: 8px;
border: none;
background: var(--color-accent);
color: #fff;
font-size: 0.875rem;
cursor: pointer;
}
.product-card__button--secondary {
background: transparent;
color: var(--color-accent);
border: 1px solid var(--color-accent);
}
/* カード幅が広い — 横並び + アクション横並び */
@container product (min-width: 280px) {
.product-card__inner {
flex-direction: row;
}
.product-card__media {
width: 40%;
aspect-ratio: auto;
min-height: 140px;
}
.product-card__content {
width: 60%;
padding: 1rem;
}
.product-card__actions {
flex-direction: row;
}
.product-card__button {
flex: 1;
}
}
/* さらに広い — 2行説明を表示 */
@container product (min-width: 360px) {
.product-card__description {
display: block;
font-size: 0.8125rem;
color: var(--color-muted);
line-height: 1.5;
margin: 0;
}
}
.product-card__description {
display: none;
}
</style>
</head>
<body>
<section class="product-grid" aria-label="おすすめ商品">
<article class="product-card">
<div class="product-card__inner">
<div class="product-card__media">商品画像</div>
<div class="product-card__content">
<h3 class="product-card__title">ワイヤレスイヤホン Pro</h3>
<p class="product-card__description">ノイズキャンセリング搭載。最大30時間再生。</p>
<p class="product-card__price">¥12,800</p>
<div class="product-card__actions">
<button type="button" class="product-card__button">カートに入れる</button>
<button type="button" class="product-card__button product-card__button--secondary">詳細</button>
</div>
</div>
</div>
</article>
<article class="product-card">
<div class="product-card__inner">
<div class="product-card__media">商品画像</div>
<div class="product-card__content">
<h3 class="product-card__title">USB-C ハブ 7in1</h3>
<p class="product-card__description">4K HDMI、SD、100W PD対応。</p>
<p class="product-card__price">¥6,480</p>
<div class="product-card__actions">
<button type="button" class="product-card__button">カートに入れる</button>
<button type="button" class="product-card__button product-card__button--secondary">詳細</button>
</div>
</div>
</div>
</article>
</section>
</body>
</html>
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)) で列数はビューポート依存だが、各カード内部のレイアウトは @container product が決める。列が狭いときは縦積み、セルが広がれば横並び——Media Query だけでは「列幅ごとに」制御できなかった領域だ。
実践例2 — ダッシュボードウィジェット
.dashboard {
display: grid;
gap: 1rem;
padding: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.dashboard {
grid-template-columns: repeat(12, 1fr);
}
.dashboard__widget--wide {
grid-column: span 8;
}
.dashboard__widget--narrow {
grid-column: span 4;
}
}
.dashboard__widget {
container-type: inline-size;
container-name: widget;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 1rem;
min-height: 120px;
}
.widget-stats {
display: grid;
gap: 0.75rem;
grid-template-columns: 1fr;
}
.widget-stats__item {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f5f9;
}
.widget-stats__label {
font-size: 0.8125rem;
color: #64748b;
}
.widget-stats__value {
font-size: clamp(1rem, 0.5rem + 3cqi, 1.5rem);
font-weight: 700;
}
/* ウィジェット幅 320px 以上 — 2列グリッド */
@container widget (min-width: 320px) {
.widget-stats {
grid-template-columns: repeat(2, 1fr);
}
.widget-stats__item {
border-bottom: none;
border-right: 1px solid #f1f5f9;
padding: 0.75rem;
}
.widget-stats__item:nth-child(2n) {
border-right: none;
}
}
/* ウィジェット幅 520px 以上 — 4列 + 大きな KPI */
@container widget (min-width: 520px) {
.widget-stats {
grid-template-columns: repeat(4, 1fr);
}
.widget-stats__item {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
border-right: 1px solid #f1f5f9;
}
.widget-stats__item:last-child {
border-right: none;
}
}
ダッシュボードでは Grid でウィジェットの占有列を決め、Container Query でウィジェット内部を決める——二段構えが保守しやすい。
実践例3 — ナビゲーションの折りたたみ
<nav class="local-nav" aria-label="セクション内ナビ">
<ul class="local-nav__list">
<li><a href="#overview">概要</a></li>
<li><a href="#spec">仕様</a></li>
<li><a href="#api">API</a></li>
<li><a href="#changelog">更新履歴</a></li>
</ul>
</nav>
<style>
.local-nav {
container-type: inline-size;
container-name: local-nav;
border-bottom: 1px solid #e2e8f0;
}
.local-nav__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.local-nav__list a {
display: block;
padding: 0.625rem 0.75rem;
text-decoration: none;
color: #334155;
border-radius: 8px;
font-size: 0.9375rem;
}
.local-nav__list a:hover {
background: #f1f5f9;
}
@container local-nav (min-width: 480px) {
.local-nav__list {
flex-direction: row;
gap: 0.5rem;
}
.local-nav__list a {
padding: 0.75rem 1rem;
}
}
@container local-nav (min-width: 720px) {
.local-nav__list {
justify-content: center;
}
}
</style>
ドキュメントサイトで、本文カラムが狭いときは縦積みナビ、幅に余裕があれば横並び——サイドバー付きレイアウトでもメインカラム幅だけで切り替わる。
Style Queries(Container Query Level 4)— 2026年の展望
Size Query だけでなく、カスタムプロパティの値で分岐する Style Query が Level 4 で議論・部分実装されている。
.card-wrapper {
container-type: inline-size;
--variant: compact;
}
@container style(--variant: compact) {
.card { padding: 0.5rem; }
}
@container style(--variant: spacious) {
.card { padding: 1.5rem; }
}
2026年時点では Style Query のブラウザ対応は Size Query ほど universal ではない。本番の主軸は Size Query とし、Style Query は「テーマ切り替えをコンテナ単位で」など限定的な実験向けと捉えるのが安全だ。進捗は Can I use と MDN の @container ページで随時確認する。
フレームワークとの統合 — React / Vue / Astro
Container Queries はフレームワーク非依存のCSS機能だ。コンポーネントのルート要素に container-type を付けるだけで動く。
// ProductCard.tsx — React 例
type ProductCardProps = {
title: string;
price: number;
imageUrl: string;
};
export function ProductCard({ title, price, imageUrl }: ProductCardProps) {
return (
<article className="product-card">
<div className="product-card__inner">
<img className="product-card__media" src={imageUrl} alt="" />
<div className="product-card__content">
<h3 className="product-card__title">{title}</h3>
<p className="product-card__price">¥{price.toLocaleString('ja-JP')}</p>
</div>
</div>
</article>
);
}
/* ProductCard.module.css またはグローバル CSS */
.product-card {
container-type: inline-size;
container-name: product;
}
@container product (min-width: 280px) {
.product-card__inner {
display: grid;
grid-template-columns: 120px 1fr;
gap: 1rem;
}
}
Tailwind CSS v3.2+ では @tailwindcss/container-queries プラグイン(v4 では統合方向)で @container ユーティリティが使える。Design Token を CSS 変数で持ち、コンテナ閾値だけ Tailwind の @md: 相当 @lg: を @container/ 名前空間で書くチームも増えている。
Astro / MDX ブログでの埋め込み
このポートフォolioのように Astro + MDX では、レイアウトスロット(<aside> / <main>)に container-type を付け、MDX 内のコンポーネントは Container Query のみを意識する——コンテンツ作者がビューポートブレークポイントを意識しないで済む。
デバッグ — Chrome DevTools で Container を可視化
Chrome DevTools を開き、Elements パネルで container-type が付いた要素を選択する
Styles パネル上部の「Container queries」バッジ、または Layout オーバーレイでコンテナ境界を確認する
Rendering タブ(Command Palette → Show Rendering)で「Layout Shift Regions」を有効にし、リサイズ時の CLS を確認する
@container ルールが効いているか、Computed スタイルで該当プロパティの打ち消し線を確認する
container-name の typo や、height 条件と inline-size の組み合わせミスを疑う
よくある原因
| 症状 | 原因 |
|---|---|
@container が一切効かない | 祖先に container-type: normal しかない。または子がコンテナの外にある |
| height 条件だけ効かない | container-type: inline-size のみ。size が必要 |
| 意図しない祖先がコンテナ | 上位の container-type がクエリを横取り。container-name で限定 |
| リサイズでガタつく | size の多用、または JS による inline style の頻繁な変更 |
パフォーマンス調査では Performance パネルで Layout → Recalculate Style の時間を、Container Query 導入前後で比較する。クリティカルレンダリングパス最適化 で述べたように、Layout の連鎖は LCP 後のインタラクションにも効く。Above-the-fold に container-type: size を大量配置していないかもチェック項目に入れる。
フォールバックと Progressive Enhancement
未対応ブラウザ向けの典型パターンは @supports だ。
/* ベース — Media Query フォールバック(狭い画面想定) */
.responsive-card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
@media (min-width: 768px) {
.responsive-card {
flex-direction: row;
}
}
/* Container Queries 対応ブラウザ — より正確な制御 */
@supports (container-type: inline-size) {
.card-slot {
container-type: inline-size;
}
@media (min-width: 768px) {
/* Media の row 指定を上書き — コンテナ基準に一本化 */
.responsive-card {
flex-direction: column;
}
}
@container (min-width: 400px) {
.responsive-card {
flex-direction: row;
}
}
}
対応ブラウザでは Container Query が優先され、非対応では従来の Media Query が効く。二重定義は保守コストが上がるため、2026年の新規案件ではフォールバックを最小限(単一列固定)に留めるチームが多い。
container-query-polyfill 等の JS ポリフィルは、ResizeObserver ベースで動作し、初期描画後にスタイルが切り替わるFOUCや CLS の原因になりやすい。レガシー要件がなければ、CSS @supports のみに留める方が Web Vitals 的にも安全だ。
アクセシビリティ — Container Queries で変えてよいもの・ダメなもの
Container Queries でレイアウト・視覚的密度を変えるのは問題ない。ただし次は守る。
- DOM の順序 — 狭いコンテナで
display: noneしすぎず、スクリーンリーダー用に重要リンクを消さない - タップターゲット — 横並びにしたボタンの
min-height: 44pxはコンテナ幅に関わらず維持 prefers-reduced-motion— コンテナ内アニメーションも@media (prefers-reduced-motion: reduce)で無効化
@container (min-width: 360px) {
.toolbar {
flex-direction: row;
}
}
@media (prefers-reduced-motion: reduce) {
.toolbar * {
transition: none !important;
}
}
パフォーマンス設計 — Containment と CRP の関係
container-type: inline-size は layout + inline-size containment を含む。理論上、コンテナ外への Layout 波及を抑えられる。ただし以下に注意する。
- ネストしたコンテナ — 親子両方に
container-typeがあると、リサイズ時に内側から再計算が走る sizeのコスト — 高さ解決のため子の intrinsic size を先に計算する必要があり、Layout フェーズが重くなりがち- CRP との関係 — Container Query 自体は追加 HTTP リクエストを発生させない。問題は初回 Layout の複雑さと、JS 連動リサイズ時の Layout Thrashing だ
クリティカルレンダリングパス最適化 で説明した Layout → Paint のパイプラインにおいて、Container Query は「正しい抽象化のための小さな Layout コスト」になりうる。ページ全体ではなくコンポメント単位に閉じれば、Media Query で無理やり上書きしていた !important 地獄を減らせる——トレードオフとしてはプラスになりやすい。
チェックリスト — 本番投入前の確認項目
| 項目 | 確認内容 |
|---|---|
| container-type | 基本は inline-size。size は必要最小限か |
| 閾値 | 320 / 480 / 640 等、Design Token と整合しているか |
| 名前 | container-name の typo がないか。意図した祖先のみがクエリ対象か |
| 単位 | cqw / cqi と rem の clamp 下限がアクセシブルか |
| フォールバック | @supports または単一列デザインで非対応時も usable か |
| 計測 | DevTools で Layout 時間、CLS を確認したか |
まとめ — 2026年のコンポーネントレスポンシブ設計
Container Queries は「コンポーネントが置かれた場所のサイズ」に応答するCSSの本命機能だ。
- ページ骨格 —
@mediaでグリッド列・グローバル余白・prefers-*を扱う - コンポーネント内部 — 親に
container-type: inline-size、子に@containerを書く - 高さも見る rare case — だけ
container-type: size - タイポグラフィ —
clamp()+cqiでコンテナ連動 - 品質 — DevTools の Container overlay と CRP 最適化 の Layout 意識
Media Query 時代は「画面サイズ = デバイス」という近似に依存していた。Container Query 時代は「コンポーネントの箱 = レイアウトコンテキスト」と明示できる。Design System・CMS 埋め込み・ダッシュボード——再利用されるUIほど、2026年のベースラインとして @container を標準装備にする価値が高い。
関連記事
この記事は フロントエンド・CSS テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。
| トピック | 記事 |
|---|---|
| CSS Grid vs Flexbox | CSS Grid vs Flexbox|どっちを使う?使い分けと実戦レイアウト |
| CSSですりガラス(Glassmorphism)を作る方法 | CSSですりガラス(Glassmorphism)を作る方法|backdrop-filterの使い方 |
| CSSで浮き出るカードを作る | CSSで浮き出るカードを作る|きれいなbox-shadowの数値設定 |
| CSSの単位はpxとremどちらが良い? | CSSの単位はpxとremどちらが良い?|基準16pxの理由と使い分け |
| line-height計算 - 読みやすい行間をフォントサイズから算出 | line-height計算 - 読みやすい行間をフォントサイズから算出 |
| Tailwind CSSでデザインシステムの色を管理する方法 | Tailwind CSSでデザインシステムの色を管理する方法 |
| 色のコントラストチェック | 色のコントラストチェック|color contrastの使い方と注意点 |
| React SSR Hydration Mismatch 原因と修正 | React SSR Hydration Mismatch 原因と修正|Next.js・Astro対応ガイド |
| HTML Popover API 完全ガイド | HTML Popover API 完全ガイド|popover・popovertarget・light dismiss・アクセシビリティ |
| Fluid Typography | Fluid Typography|clamp() で可変フォント |
| content-visibility: auto 完全ガイド | content-visibility: auto 完全ガイド|遅延レンダリングと長尺ページ最適化 2026 |
| CSS JS Minify 圧縮 | CSS JS Minify 圧縮|本番向けファイルサイズ削減 |