テクニック

CSSコンテナクエリ(container)実装ガイド|親要素の幅でレイアウトを切り替える

この記事の対象: 同じカードを複数の幅で使い回すことになった制作者
読了時間: 約9分

サイドバーに置いたカードと、メインカラムに置いた同じカード。ビューポート幅は同じなのに、置き場所の幅が違うせいで片方だけ崩れる。メディアクエリで直そうとすると「サイドバーにあるときだけ」という条件が書けず、結局クラスを増やして分岐させる——この行き止まりを解消するのがコンテナクエリです。判定の基準をビューポートから「親要素の幅」に移すだけで、部品が自分の置かれた場所を見てレイアウトを切り替えられるようになります。この記事では、素のHTML/CSSだけで動くカードを組み立てながら、書き方・対応状況の確認方法・つまずきやすい点までを通しで扱います。


前提と動作環境

前提と動作環境

この記事のコードは素のHTML/CSSだけで動きます。ビルドツールもフレームワークも不要で、.html ファイルをブラウザで開けば確認できます。PostCSSもSassも使いません。

CMSに組み込む場合の置き場所は次のとおりです。

環境 CSSの置き場所 HTMLの置き場所
静的サイト assets/css/style.css など任意のCSS 該当のHTMLファイル
WordPress(クラシック/ブロック共通) 子テーマの style.css テンプレートまたはブロックパターン
Shopify(Online Store 2.0) assets/component-card.css+セクションから stylesheet_tag で読み込み sections/ または snippets/ のLiquid

WordPressの子テーマをまだ用意していない場合は、先に最小構成を作っておくと安全です。

対応ブラウザは、コンテナクエリの中核である @container(size query)が Chrome 105 / Edge 105 / Safari 16 / Firefox 110 以降。コンテナ単位(cqw など)も同じタイミングで入っています。後述する style() クエリや @container scroll-state() は対応時期が別なので、同じ扱いにしないでください。確認方法は後の章で書きます。


メディアクエリとコンテナクエリの違い

メディアクエリとコンテナクエリの違い

見ているものが違うだけです。メディアクエリはビューポート(ブラウザの表示領域)、コンテナクエリは指定した祖先要素のサイズを見ます。

判定の基準がどこにあるか

コンテナクエリ

  • 親要素の幅で判定する
  • 部品を置く場所を選ばない
  • CSSが部品ごとに閉じる

メディアクエリ

  • ビューポート幅で判定する
  • 置き場所ごとに分岐が要る
  • ページ側の事情がCSSに漏れる

図のとおり、コンテナクエリでは「この部品はどこに置かれているか」をCSS側が知らなくて済みます。カード側のCSSを一度書けば、メインでもサイドバーでもフッターでも同じファイルが効きます。

ただし両方を使います。使い分けの目安は下の表です。

判断したいこと 使うもの
ページ全体のカラム数(1カラム↔2カラム) メディアクエリ
グローバルナビをハンバーガーに畳む メディアクエリ
印刷用スタイル メディアクエリ(@media print)
カード内の画像を横並び↔縦積みに切り替える コンテナクエリ
同じ部品を幅の違う複数箇所で使う コンテナクエリ
文字サイズを入れ物の幅に追従させる コンテナクエリ(cqi 単位)

ページの骨格はメディアクエリ、骨格の中に入る部品はコンテナクエリ。この線引きで迷うことはほぼなくなります。


最小構成のコードを書く

コンテナクエリは2ステップです。(1)親を「コンテナ」だと宣言する(2)その中の要素に @container を書く。宣言していない親は測定対象になりません。ここを飛ばして @container だけ書くのが最も多い失敗です。

そのまま動くHTMLとCSS

以下を container-query-demo.html として保存し、ブラウザで開いてください。ウィンドウ幅ではなく、カードを包む枠の幅で見た目が変わることが確認できます。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>コンテナクエリの動作確認</title>
<style>
/* ページ骨格。ここはメディアクエリの担当 */
.layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  max-width: 1100px;
  margin: 0 auto;
  padding: 24px;
  font-family: system-ui, sans-serif;
}
@media (min-width: 900px) {
  .layout { grid-template-columns: 1fr 300px; }
}

/* (1) 親をコンテナとして宣言する。これが無いと @container は効かない */
.card-area {
  container-type: inline-size;
  container-name: card;
}

/* カードの既定形(狭いとき)。縦積み */
.card {
  display: grid;
  gap: 12px;
  border: 1px solid #ddd;
  border-radius: 12px;
  padding: 16px;
  background: #fff;
}
.card__thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 8px;
}
.card__title {
  margin: 0;
  font-size: 1.05rem;
  line-height: 1.5;
}
.card__text {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.8;
  color: #555;
}

/* (2) コンテナ card の幅が 420px 以上なら横並びにする */
@container card (min-width: 420px) {
  .card {
    grid-template-columns: 200px 1fr;
    align-items: start;
    gap: 20px;
  }
  .card__thumb { aspect-ratio: 4 / 3; }
  .card__title { font-size: 1.25rem; }
}

/* さらに広いとき。サムネを大きく、本文を3行まで */
@container card (min-width: 680px) {
  .card { grid-template-columns: 300px 1fr; }
  .card__title { font-size: 1.5rem; }
}

.area-label {
  margin: 0 0 8px;
  font-size: 0.8rem;
  color: #888;
}
</style>
</head>
<body>
<div class="layout">

  <div>
    <div class="card-area">
      <p class="area-label">メインカラム側(広い)</p>
      <article class="card">
        <img class="card__thumb" src="https://placehold.jp/600x400.png" alt="">
        <div>
          <h2 class="card__title">同じHTML・同じCSSのカード</h2>
          <p class="card__text">親要素の幅が420pxを超えたので横並びになっています。ブラウザ幅ではなく、このカードを包む枠の幅で判定しています。</p>
        </div>
      </article>
    </div>
  </div>

  <aside>
    <div class="card-area">
      <p class="area-label">サイドバー側(狭い)</p>
      <article class="card">
        <img class="card__thumb" src="https://placehold.jp/600x400.png" alt="">
        <div>
          <h2 class="card__title">同じHTML・同じCSSのカード</h2>
          <p class="card__text">親要素が420px未満なので縦積みのままです。カード側のCSSには「サイドバーのとき」という条件が一行もありません。</p>
        </div>
      </article>
    </div>
  </aside>

</div>
</body>
</html>

自分の案件に移すとき変更する箇所

上のコードを持っていくときに、読者側で書き換えるのは次の5つです。

  • card-area / card のクラス名 — 既存のCSS設計に合わせて改名する。container-name の値(card)と @container の直後の名前は必ずセットで変える
  • 420px / 680px のブレークポイント — カードの中身次第。後述の決め方を参照
  • 200px / 300px(サムネの幅) — 使う画像の比率に合わせる
  • aspect-ratio の値 — サムネの縦横比
  • src のプレースホルダ画像 — 実画像に差し替える。alt は装飾目的なら空、内容を伝えるなら文章を入れる

コンテナクエリでカードレイアウトを組んでいる実例は、Soreiine!!のギャラリーでも見比べられます。

レスポンシブなカードレイアウト事例|Soreiine!! ギャラリー


container-type と container-name の指定を理解する

ここを曖昧なまま進めると、後で原因の分からない崩れに当たります。

container-type の3つの値

値 測れるもの 副作用
inline-size 横幅(書字方向のインライン方向)だけ インライン方向のサイズが内容から独立する
size 幅と高さの両方 幅と高さの両方が内容から独立する。高さを明示しないと潰れる
normal サイズは測れない(style() クエリ専用) レイアウトへの副作用なし

実務では inline-size が9割 です。size は高さを自分で決めている要素(height 指定済みのモーダル、固定高のヒーローなど)でしか使えません。中身の高さで伸びる要素に size を付けると高さが0になって消えます。

container-type を付けると、その要素にはコンテインメント(内部の影響を外に出さない封じ込め)が働きます。だから測れるようになるかわりに、中身の幅で親が広がるという挙動は失われます。width: fit-content に頼っていた箇所は要注意です。

container-name は省略できるが、付ける

名前を省くと「最も近い、条件に合う祖先コンテナ」が自動で選ばれます。入れ子が増えたとき、どのコンテナを見ているのか読めなくなるので、名前は付けてください。

/* 省略記法。type と name をまとめて書ける */
.card-area {
  container: card / inline-size;
}

container: <name> / <type> の順です。逆に書くと効きません。名前を複数付けることもできます。

.card-area {
  container-type: inline-size;
  container-name: card sidebar-slot;
}

こう書くと @container card (...) でも @container sidebar-slot (...) でも拾えます。汎用名と用途名を両方持たせたいときに使います。


幅に追従する文字サイズ(cqi 単位)

コンテナクエリと同時に使えるようになった単位があります。よく使うのは cqi(コンテナのインライン方向サイズの1%)です。

.card__title {
  /* 最小1rem、上限1.75rem、その間はコンテナ幅の4%に追従 */
  font-size: clamp(1rem, 4cqi, 1.75rem);
  line-height: 1.4;
}

vw を使ったタイポグラフィとの違いは、ビューポートではなくコンテナに追従すること。サイドバーに置いたときだけ文字が巨大になる、という vw のよくある事故が起きません。

単位 基準
cqw / cqh コンテナの幅 / 高さの1%
cqi / cqb コンテナのインライン方向 / ブロック方向の1%
cqmin / cqmax cqi と cqb の小さいほう / 大きいほう

日本語の縦書きを扱わないなら cqi と cqw はほぼ同義ですが、cqi を使う癖にしておくと縦書き対応時に書き直しが要りません。

clamp() で上限下限を必ず付けてください。素の 4cqi だけだと、コンテナが極端に狭いときに文字が読めない大きさまで縮みます。


ブレークポイントの決め方

ブレークポイントの決め方

コンテナクエリでは、ブレークポイントをデバイス幅ではなく「その部品が破綻する幅」で決めます。手順は3つです。

コンテナ側ブレークポイントの決め方
1. 狭い形から書く縦積み・1カラムを既定にする
2. 枠を広げて壊れる点を探すDevToolsで親要素の幅を直接いじる
3. その手前を境界にする切りのいい数字に丸める

2の作業がキモです。ブラウザ幅ではなく、DevToolsのStylesパネルで .card-area に width: 380px などを一時的に当てて、数字を動かしながら見ます。「サムネの横に見出しが2文字しか入らなくなった」地点の少し手前が境界です。

境界の数は多くて3つに抑えてください。5つ6つと増やすと、どの幅でどの形になるのかを自分で説明できなくなります。だいたい「縦積み」「横並び」「横並び・サムネ大」の3形態で足ります。

レイアウト全体の組み方そのものを迷っている場合は、カラム数の判断から先に固めたほうが早いです。


対応状況を実装側で確認する

「対応しているか」はカタログで調べるだけでなく、コード側で分岐しておけます。

CSSで確認する

/* コンテナクエリに未対応なら、こちらが効く */
.card { display: grid; gap: 12px; }

@supports (container-type: inline-size) {
  .card-area { container: card / inline-size; }

  @container card (min-width: 420px) {
    .card { grid-template-columns: 200px 1fr; }
  }
}

未対応ブラウザでは @supports の中身がまるごと無視され、縦積みのまま表示されます。狭い形を既定にしておけば、これがそのままフォールバックになります。逆に横並びを既定にすると、未対応環境で潰れた横並びが残ります。順番が重要です。

JavaScriptで確認する

計測やA/Bのために判定したいときだけ使います。表示の切り替えはCSSの @supports に任せてください。

<script>
  const supported = CSS.supports('container-type', 'inline-size');
  document.documentElement.classList.toggle('has-cq', supported);
  console.log('container query:', supported ? 'supported' : 'not supported');
</script>

CSS.supports() は第1引数にプロパティ名、第2引数に値を渡す形と、CSS.supports('(container-type: inline-size)') のように1文字列で渡す形の両方が使えます。上は前者です。

実機で確認する

手元のChromeで動いても、確認すべきは次の3つです。

  • iOS Safari — バージョン16未満の端末が残っていないか。iOS 15止まりのiPad等が実際に使われていないかをアクセス解析で見る
  • Android標準ブラウザ / WebView — アプリ内ブラウザ経由の流入がある場合
  • 社内で指定されている旧環境 — 官公庁系・社内システム連携がある案件では明示的に聞く

判定はアクセス解析のブラウザ・OSバージョン別のセッション数で行います。そこに1%も出てこない環境のために実装を歪めないこと。ただしその1%が主要顧客層かどうかだけは確かめてください。


Shopify・WordPressへの組み込み

Shopify(Online Store 2.0)

セクション単位でカードを使い回すなら、コンテナ宣言はセクションのラッパーに置きます。

assets/component-ts-card.css を新規作成:

.ts-card-area { container: ts-card / inline-size; }

.ts-card {
  display: grid;
  gap: 12px;
  padding: 16px;
  border-radius: 12px;
  background: rgb(var(--color-background));
}

@container ts-card (min-width: 420px) {
  .ts-card { grid-template-columns: 200px 1fr; gap: 20px; }
}

sections/ts-card-list.liquid の先頭で読み込みます:

{{ 'component-ts-card.css' | asset_url | stylesheet_tag }}

<div class="ts-card-area">
  {%- for block in section.blocks -%}
    <article class="ts-card">
      {%- if block.settings.image -%}
        {{ block.settings.image | image_url: width: 600 | image_tag: class: 'ts-card__thumb', loading: 'lazy' }}
      {%- endif -%}
      <div>
        <h3 class="ts-card__title">{{ block.settings.title }}</h3>
        <p class="ts-card__text">{{ block.settings.text }}</p>
      </div>
    </article>
  {%- endfor -%}
</div>

クラス名の ts- は自分のプレフィックスに置き換えてください。Dawn本体のクラス(card, card-wrapper など)をそのまま使わないこと。テーマ更新で衝突します。この方針の詳細はこちら。

WordPress(子テーマ)

子テーマの style.css に上のCSSを書き、テンプレート側でラッパーを出します。ブロックテーマでブロックパターン化するなら、パターン側のラッパーに ts-card-area を持たせます。

<?php // 子テーマの任意テンプレート内 ?>
<div class="ts-card-area">
  <?php while ( have_posts() ) : the_post(); ?>
    <article class="ts-card">
      <?php if ( has_post_thumbnail() ) : ?>
        <?php the_post_thumbnail( 'medium', array( 'class' => 'ts-card__thumb', 'loading' => 'lazy' ) ); ?>
      <?php endif; ?>
      <div>
        <h3 class="ts-card__title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
        <p class="ts-card__text"><?php echo esc_html( get_the_excerpt() ); ?></p>
      </div>
    </article>
  <?php endwhile; ?>
</div>

編集者が触るブロックパターンに組み込む場合は、崩れない作りにしておくところまでがセットです。


うまくいかないとき

@container を書いたのに何も変わらない

原因はほぼこの3つです。上から順に見てください。

  • 親に container-type が無い — 最頻。@container は「祖先にコンテナ宣言がある」ことが前提
  • container-name と @container の名前が一致していない — 片方だけ改名した
  • コンテナ自身にスタイルを当てようとしている — @container の中で .card-area {} を書いても効きません。コンテナ自身は自分のクエリの対象外です。その要素を変えたいなら、さらに外側にコンテナを作るか、:has() などで別経路を取る

横並びにならず、常に狭い形のまま

container-type: inline-size を付けた要素の幅が0か、内容幅に縮んでいる可能性があります。コンテナ宣言をした要素が display: inline や float の中にあると、幅が期待どおりに広がりません。まずDevToolsでその要素の実寸を見てください。width: 100% や display: block を明示すれば直ることが多いです。

高さが潰れて中身が消えた

container-type: size を使っています。inline-size に変えてください。size は高さも封じ込めるため、高さを明示していない要素では0になります。

Grid / Flexアイテムをコンテナにしたら幅がおかしい

Gridセル自体に container-type: inline-size を付けると、grid-template-columns: auto などの内容ベースのサイズ計算が効かなくなります。セルの中にもう1枚 div を挟んで、そちらをコンテナにするのが安全です。

<div class="grid">
  <div class="grid__cell">
    <div class="card-area">  <!-- ここをコンテナにする -->
      <article class="card">...</article>
    </div>
  </div>
</div>

レイアウトシフト(CLS)が出る

コンテナクエリ自体はCLSの原因になりませんが、画像に幅・高さが無いと切り替わった瞬間に高さが確定せずガタつきます。aspect-ratio か width/height 属性を必ず入れてください。上のサンプルで aspect-ratio を書いているのはこのためです。

CSSファイルが読まれていない

Shopifyでは stylesheet_tag の位置、WordPressでは wp_enqueue_style の依存指定を確認。DevToolsのNetworkタブで当該CSSが200で返っているかを見るのが最短です。キャッシュが残っている場合はハードリロード(Ctrl/Cmd + Shift + R)。

style() クエリが動かない

@container style(--theme: dark) のようなスタイルクエリはサイズクエリとは対応時期が別です。カスタムプロパティを条件にする形が先行して実装されていて、任意のプロパティに広げる部分はまだ揃っていません。使う前に必ず @supports で囲み、未対応時の見た目を確認してから入れてください。サイズクエリが動いたからスタイルクエリも動く、とは考えないこと。


よくある質問

コンテナクエリを使えばメディアクエリは不要になりますか

なりません。ページ全体のカラム数、グローバルナビの畳み方、印刷用スタイルはビューポートを見る必要があるのでメディアクエリの担当です。役割を分けて併用するのが正しい形で、どちらかに寄せる話ではありません。

未対応ブラウザではどう見えますか

@container ブロックの中身が丸ごと無視されます。そのため、狭い幅向けのレイアウトを既定として先に書き、広いときの上書きを @container の中に入れる順番にしておけば、未対応環境では縦積みのまま正常に表示されます。逆順に書くと未対応環境で崩れます。

container-type を付けるとページが重くなりませんか

コンテインメントが働くことで、その要素の内部と外部のレイアウト計算が切り離されます。むしろ再計算の範囲が狭まるので、無闇に重くなるものではありません。ただしページ内の要素すべてにコンテナ宣言を付ける必要はなく、実際に切り替えたい部品の親だけに付けてください。

ブレークポイントは何pxにすればいいですか

デバイス幅の慣習的な数字(768px、1024px)をそのまま持ち込まないでください。コンテナクエリの境界は「その部品の中身が破綻する幅」で決めます。DevToolsで親要素の幅を直接動かし、サムネの横に見出しが入らなくなる地点の手前を採用するのが実務的な手順です。

既存サイトに後から入れられますか

入れられます。既存のメディアクエリを消す必要はなく、崩れている部品だけをコンテナクエリに置き換える形で段階的に移行できます。まず該当部品の親に container: name / inline-size を足し、ビューポート基準だった分岐をコンテナ基準に書き換えるところから始めてください。


まとめ

コンテナクエリでやることは2つだけです。親に container: 名前 / inline-size を書く。子の切り替えを @container 名前 (min-width: ...) に入れる。これで部品が置き場所を選ばなくなります。

今日の作業を終えるための順番は次のとおりです。

  1. 崩れているカードの親要素に container-type: inline-size と container-name を足す
  2. 既定は狭い形(縦積み)で書く
  3. DevToolsで親の幅を動かし、破綻する点の手前を境界にする
  4. @supports (container-type: inline-size) で囲んでフォールバックを確保する
  5. サイドバー・メイン・フッターの3箇所に同じHTMLを置いて、CSSを一行も足さずに済むか確かめる

5番目まで通れば、その部品は完成です。次に同じカードを別の幅の場所へ置くとき、CSSを触る必要はありません。