Inside cmn!

山本 慎二

今更ながらCSSコンテナクエリー入門 — @container から cqi・cqw などの単位まで、具体的な使用例とともに解説

UI/UXプログラミングTips

この記事は誰向け?

  • フロントエンドエンジニアの方:コンテナクエリーが便利そうだとは知っていても、なかなかキャッチアップできていなかった方
  • デザイナーの方:「配置する場所によってスタイルが自動で変わるコンポーネント」という概念をコードレベルで理解したい方

コンテナクエリーとは

コンテナクエリーとはビューポート(ブラウザ画面)の幅や出力デバイスの特性に応じてスタイルを切り替えるメディアクエリーに対して、親要素(コンテナ)のサイズやスタイルに応じてスタイルを切り替えることのできる機能です。

文章だけだとピンとこないという方は、コンテナクエリーの機能を体感できるデモを用意しましたので、ぜひ遊んでみてくださいね。
ドラッグ&ドロップでの移動にJSを使っていますが、CSSやクラスはそのままでスタイルが変わります!

Container Query Demo Playground

ブラウザサポート状況

2022年8月にChrome/Edgeでサポートされ、その後同年9月にSafari、2023年2月にFirefoxでサポートされ、主要ブラウザ全体での対応が完了しました。

2023年末頃には世界のブラウザシェアにおけるカバレッジが約93%に到達し、実運用でも採用されるようになってきました。少し前のデータですが、2025年6月時点の日本国内のPC+モバイルのブラウザシェアから判断すると、国内でのカバレッジは約96〜97%に達しており、現在はフォールバックなしで利用できると概ね言って差し支えない状況です。

メディアクエリーとの違い

2022年以前からCSSを書いてきたフロントエンドエンジニアの方は、たとえば商品一覧ページのカードを実装する際、検索結果ページや特集ページなど親要素の幅が変わる場所に同じカードを使い回そうとすると、モディファイアクラス(.card--small など)を付与したり、場所ごとに別のセレクターでスタイルを上書きしたりと、なかなか面倒な対応を迫られてきたかと思います。

コンテナクエリーを利用すると、一覧セクションの幅に応じてスマートにスタイルを変更できるため、同じコンポーネントをそのまま複数箇所で使い回せます。実装がシンプルになり、保守性も向上します。

【メディアクエリー】 【コンテナクエリー】
ビューポート幅を基準

場所を選ぶ実装
親コンテナ幅を基準

どこに置いても自律的に変化

書き方

基本構文

コンテナクエリーは2ステップで実装します。

① 親コンテナに container-type を設定する

.card-wrapper {
  container-type: inline-size;  /* 幅を基準にする */
  container-name: card;         /* コンテナに名前をつける(省略可) */
}

② @container で子要素のスタイルを記述する

@container card (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
}

使い分け

container-type には3つの値があります。

基準にできる軸 主な用途
inline-size インライン軸(通常は)のみ カード・ナビ・ほぼ全ての実用ケース
size 幅・高さ両方 高さが固定されたパネル・モーダル
normal サイズ基準不可(スタイルクエリーのみ カスタムプロパティによる分岐

inline-size(最もよく使う)

幅だけを基準にしたいとき。高さは封じ込めないので、コンテンツの縦方向の自然な伸縮を妨げません。実用ケースのほぼすべてはこれで対応できます。

.card-wrapper {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
}

size(使う場面は限定的)

幅・高さ両方を基準にしたいとき。ただしコンテナの高さを外部から明示的に与えないと正しく機能しないという制約があります。

.modal-body {
  container-type: size;
  height: 400px; /* 高さを明示しないとcqhが0になる */
}

@container (max-height: 300px) {
  .modal__footer {
    display: none;
  }
}

normal(スタイルクエリー専用)

サイズではなくCSSカスタムプロパティの値で分岐したいとき。サイズ封じ込めが発生しないため、通常のブロックとして振る舞います。

.button-wrapper {
  container-type: normal;
  container-name: btn;
  --variant: outlined;
}

@container btn style(--variant: outlined) {
  .button {
    background: transparent;
    border: 2px solid currentColor;
  }
}

実装のコツ:入れ子構造にしておく

サイズ判定の基準となる親コンテナと、スタイルを変えたい子要素は同じ要素を兼ねることができません。そのため、シンプルなコンポーネントでも入れ子構造にしておくと実装がスムーズです。

たとえば商品一覧で、一覧カラム数とカード内のスタイルを個別に制御したい場合を考えてみます。

制御しにくい例(フラットな構造)

<!-- .list-item がコンテナにもなるし子にもなってしまい、制御が難しい -->
<div class="list-wrapper">  ← 親コンテナA
  <ul class="list">
    <li class="list-item">  ← 親コンテナB かつ スタイル対象
      <img class="list-item__photo">
      <p class="list-item__price">¥1,980</p>
      <a class="list-item__cta">カートに入れる</a>
    </li>
  </ul>
</div>

制御しやすい例(ラッパーで分離)

<!-- .list-item-wrapper がコンテナ、.list-item が子 という役割を明確に分離 -->
<div class="list-wrapper">
  <ul class="list">
    <li class="list-item-wrapper">  ← コンテナ(container-type: inline-size)
      <div class="list-item">       ← スタイル対象
        <img class="list-item__photo">
        <p class="list-item__price">¥1,980</p>
        <a class="list-item__cta">カートに入れる</a>
      </div>
    </li>
  </ul>
</div>
.list-item-wrapper {
  container-type: inline-size;
  container-name: list-item;
}

@container list-item (min-width: 360px) {
  .list-item {
    display: flex;
    flex-direction: row;
    gap: 1rem;
  }
}

コンテナクエリー単位(CQ Units)

@container と同時に導入されたコンテナのサイズを基準にした相対単位も使えます。vwvh のコンテナ版と考えるとわかりやすいです。

単位 意味
cqw コンテナのの1%
cqh コンテナの高さの1%
cqi コンテナのインライン軸(通常は幅)の1%
cqb コンテナのブロック軸(通常は高さ)の1%
cqmin cqi と cqb の小さい方の1%
cqmax cqi と cqb の大きい方の1%

実用上は cqi が最もよく使われます。writing-mode に依存しない論理的な単位のため、多言語対応にも強いです。clamp() との組み合わせが定番パターンで、コンポーネント単位で流動的なタイポグラフィを実現できます。

.card-wrapper {
  container-type: inline-size;
}

.card__title {
  /* ビューポートでなくカードの幅に追従してフォントが伸縮 */
  font-size: clamp(1rem, 4cqi, 2rem);
}

.card__thumbnail {
  width: 30cqi;
}

使用例

1. カード

最も代表的なユースケース。狭いコンテナでは縦積み、広いコンテナでは横並びに自動切替します。
<article class="cq-card">
  <div class="cq-card__layout">
    <figure class="cq-card__media">
      <img src="./product.jpg" alt="Gray running shoes">
    </figure>
    <div class="cq-card__body">
      <p class="cq-card__eyebrow">Featured Item</p>
      <h3 class="cq-card__title">Adaptive Product Card</h3>
      <p class="cq-card__text">狭いコンテナでは読みやすく縦に積み、広いコンテナでは画像と本文の関係を強く見せます。</p>
      <div class="cq-card__meta">
        <span class="cq-card__price">¥12,000</span>
        <button class="cq-card__button" type="button">カートに入れる</button>
      </div>
    </div>
  </div>
</article>
.cq-card {
  container-type: inline-size;
}

.cq-card__layout {
  display: grid;
  grid-template-columns: minmax(0, 10.5rem) minmax(0, 1fr);
  gap: 1rem;
  min-width: 0;
  align-items: center;
}

/* コンテナ幅に応じて流動的に伸縮するフォントサイズ */
.cq-card__title {
  font-size: clamp(0.9rem, 3cqi, 1.25rem);
}

.cq-card__text {
  font-size: clamp(0.75rem, 2.5cqi, 0.9rem);
}

/* 狭いコンテナ向け:縦積みレイアウト */
@container (max-width: 30rem) {
  .cq-card__layout {
    grid-template-columns: 1fr;
  }

  .cq-card__meta {
    grid-template-columns: 1fr;
    gap: 12px;
  }
}

/* 広いコンテナ向け:情報量を増やす */
@container (min-width: 50rem) {
  .cq-card__title {
    font-size: min(2rem, 3cqw);
  }

  .cq-card__layout {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cq-card__meta {
    grid-template-columns: 1fr;
    gap: 12px;
  }
}

2. CTAボタン

配置するコンテナの幅に応じてボタンのサイズやレイアウトを変えます。広いコンテナではアクションを横並びに、狭いコンテナではボタンを大きくして縦に積みます。

<section class="cq-cta" data-variant="accent">
  <div class="cq-cta__layout">
    <div class="cq-cta__copy">
      <p class="cq-cta__eyebrow">Team Access</p>
      <h3 class="cq-cta__title">Invite your team to this workspace</h3>
      <p class="cq-cta__text">広いコンテナではアクションを横並びに、狭いコンテナではボタンを大きくして縦に積みます。</p>
    </div>
    <div class="cq-cta__actions">
      <button class="cq-cta__action cq-cta__action--primary" type="button">Invite members</button>
      <button class="cq-cta__action cq-cta__action--secondary" type="button">Learn more</button>
    </div>
  </div>
</section>
.cq-cta {
  container-type: inline-size;
}

.cq-cta__layout {
  display: grid;
  gap: 1rem;
  min-width: 0;
}

/* コンテナ幅に応じて流動的に伸縮するフォントサイズ */
.cq-cta__title {
  font-size: clamp(1rem, 3cqi, 1.25rem);
}

.cq-cta__text {
  font-size: clamp(0.75rem, 2.5cqi, 0.9rem);
}

.cq-cta__actions {
  display: flex;
  gap: 0.75rem;
  min-width: 0;
  flex-wrap: wrap;
}

/* 広いコンテナ向け:テキストとアクションを横並びに */
@container (min-width: 50rem) {
  .cq-cta__layout {
    grid-template-columns: minmax(0, 1.4fr) auto;
    align-items: center;
  }

  .cq-cta__actions {
    justify-content: end;
    align-items: center;
  }
}

/* 狭いコンテナ向け:ボタンを縦積みしフル幅に */
@container (max-width: 30rem) {
  .cq-cta__actions {
    flex-direction: column;
  }

  .cq-cta__action {
    width: 100%;
  }
}

3. テーブル

狭いコンテナではテーブルが崩れてしまう問題をコンテナクエリーで解決します。狭いときは各行をカード形式で縦積み表示に切り替えます。

<section class="cq-table">
  <div class="cq-table__frame">
    <table>
      <thead>
        <tr>
          <th>Plan</th>
          <th>Seats</th>
          <th>Status</th>
          <th>Renewal</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td data-label="Plan">Starter</td>
          <td data-label="Seats">5</td>
          <td data-label="Status">Active</td>
          <td data-label="Renewal">2026-04-18</td>
        </tr>
        <tr>
          <td data-label="Plan">Growth</td>
          <td data-label="Seats">12</td>
          <td data-label="Status">Trial</td>
          <td data-label="Renewal">2026-05-01</td>
        </tr>
      </tbody>
    </table>
  </div>
</section>
.cq-table {
  container-type: inline-size;
}

.cq-table table {
  width: 100%;
  border-collapse: collapse;
}

.cq-table th,
.cq-table td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #ddd;
}

/* 狭いコンテナ向け:カード型の縦積みに切り替え */
@container (max-width: 50rem) {
  .cq-table thead {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .cq-table tbody tr {
    display: grid;
    gap: 0.5rem;
    padding: 1rem;
    border: 1px solid #ddd;
    margin-bottom: 1rem;
  }

  .cq-table td {
    border: none;
    display: flex;
    align-items: center;
  }

  /* data-label属性の値をラベルとして表示 */
  .cq-table td::before {
    content: attr(data-label);
    font-weight: 700;
    margin-right: 1em;
    min-width: 80px;
  }
}

4. ニュース一覧(oEmbed)

ニュース記事やブログ投稿の一覧に、oEmbedで取得したメディア(サムネイル・動画など)を組み合わせるパターンです。コンテナ幅に応じてサムネイルの表示・非表示を切り替えます。

<section class="cq-news">
  <ul class="cq-news__list">
    <li class="cq-news__item">
      <div class="cq-news__media" aria-hidden="true"></div>
      <div class="cq-news__body">
        <p class="cq-news__meta">Product Update · 2026-03-18</p>
        <h3 class="cq-news__title">Container queries now power embedded media summaries</h3>
        <p class="cq-news__excerpt">サムネイルや動画プレビューを含むニュースカードを、配置先の幅に合わせて切り替えます。</p>
      </div>
    </li>
  </ul>
</section>
.cq-news {
  container-type: inline-size;
}

.cq-news__item {
  display: grid;
  grid-template-columns: 9rem 1fr;
  gap: 1rem;
}

/* コンテナ幅に応じて流動的に伸縮するフォントサイズ */
.cq-news__title {
  font-size: clamp(0.95rem, 3cqi, 1.15rem);
}

.cq-news__excerpt {
  font-size: clamp(0.75rem, 2.3cqi, 0.9rem);
}

/* 狭いコンテナ向け:メディアを非表示、テキスト優先 */
@container (max-width: 31rem) {
  .cq-news__item {
    grid-template-columns: 1fr;
  }

  .cq-news__media {
    display: none;
  }
}

まとめ

コンテナクエリーは「コンポーネントが置かれた場所を自分で認識してスタイルを変える」という、メディアクエリーでは実現できなかった設計を可能にします。

新規実装にはもちろん、既存のコンポーネントのリファクタリングにも取り入れやすい機能です。ぜひ試してみてくださいね!

Written by

山本 慎二 Shinji Yamamoto

front-end engineer

1987年に東京で生まれたが、1歳からは北海道で育つ。
2010年に札幌市立大学デザイン学部製品デザインコースを卒業後、イベント運営を行う会社に勤務し業務の一環でHTML/CSSに触れる。
2016年にcommono立ち上げ時から参加し、フロントエンドエンジニアとして学びながら現在に至る。

Q1. commonoに参加した理由
矢野さんが立ち上げメンバーを探しているときに、知人の紹介でお会いし、ここでなら前向きにワクワクできるようなことができると感じて参加させていただきました。
Q2. わたしの偏愛
マイホーム(今年こそピザ窯を)
Q3. 今後commonoというフィールドでやりたいこと
クライアント様やデザイナーと一緒にユーザーの心に残るような体験をWEBで表現する手伝いをしていきたいです。

Recommend

お問い合わせ

    個人情報利用目的への同意

    ※このサイトはCloudflare Turnstileよって保護されています。
    ※お客様が入力した個人情報はSSL暗号化信号によって保護されています。

    個人情報の利用目的
    このお問い合わせフォームでご提供いただく個人情報は、お問合せ等に対する回答や資料送付、その他ご連絡のために利用します。
    当個人情報を第三者に提供することはありません。
    当個人情報の取扱いを委託することはありません。
    必要事項を全てご入力下さい。入力内容に不備がある場合は、ご返信しかねる場合がございます。
    内容により、お時間をいただく場合がございます。
    当個人情報の利用目的の通知、開示、内容の訂正・追加または削除、利用の停止・消去および第三者への提供の停止(「開示等」といいます。)を受け付けております。
    開示等の求めは、info@commono.co.jpで受け付けます。
    統計ツールやクッキー、ウェブビーコン等を用いてご利用状況を調査していますが、これによる個人情報の取得、利用は行っておりません。
    当社の個人情報保護に対する取り組みは「Privacy Policy」を必ずご確認ください。

    COMMONO株式会社
    E-mail:info@commono.co.jp
    個人情報保護方針、個人情報の取扱いについて
    PRIVACY POLICY

    現在準備中です

    お気に入り

    お気に入りから削除

    選択したアイテムをお気に入りから削除しますか?