ご注意
この記事は AI のサポートを受けていますが
1. おさらい:なぜ Container Query が必要か
第13回で話しましたが、media query が知っているのは viewport だけ — カードが sidebar 280px の中にあるのか、4列 grid の中にあるのかは分かりません。結果としてよくあるのは、バリアントを増やす、props でレイアウトを調整する、CSS が膨らむのにコンポーネントが再利用できない、というパターンです。
Container Query はまさにそこを解決します。コンポーネントが 親の幅はいくつか — そして本記事からは 親のテーマは何か — を自分で聞く。画面サイズを聞くのではなく。
第13回をまだ読んでいない方は、レンダリングパイプラインと
ProfileCardの基本を先にどうぞ。本記事は「シーズン2」であって、総集編ではありません。
2. Container Query 基本 — 覚えておく構文
Container Query は 親 container のサイズまたはスタイル に基づいてスタイルを適用します。viewport ではありません。3ステップだけ — 注文するように覚えてください:container-type を宣言 → @container で条件を書く → 条件が真なら子のスタイルが適用される。
2.1. container-type — 重要な値
| 値 | 意味 | 用途 |
|---|---|---|
inline-size |
幅(inline 軸)で query | 最も一般的 — 横方向の responsive |
size |
幅と高さの両方で query | 両軸が必要なとき |
normal |
size query 用の container ではない | デフォルト — size query には使わない |
/* container の宣言 */
.card-container {
container-type: inline-size; /* 幅のみ query */
/* または */
container-type: size; /* 幅と高さの両方 */
}
2.2. 基本構文
/* 名前なし — 最も近い container を query */
@container (max-width: 400px) {
.child {
/* style */
}
}
/* 名前あり — 特定の container を query */
@container sidebar (max-width: 400px) {
.child {
/* style */
}
}
/* Style Query — カスタムプロパティで query */
@container style(--theme: dark) {
.child {
/* style */
}
}
3. Container Naming — container に名前を付ける
container が1つなら簡単 — 最も近い container を query すれば OK。でも実際のページは main の中に sidebar、sidebar の中に grid、その中に card... そんなとき 名前を付ける と、query したい container を正確に指定でき、CSS が「間違った親」を掴むのを防げます。
3.1. container-name — 名前の付け方
2つの書き方 — 手慣れの方を選んでください:
/* 方法1: container-name を個別に */
.card-container {
container-name: card;
container-type: inline-size;
}
/* 方法2: container のショートハンド */
.card-container {
container: card / inline-size; /* 名前 / タイプ */
}
3.2. @container で名前を使う
名前があれば query は正確。名前を省略するとブラウザは 最も近い container を使います — 1つだけなら便利、多段ネストだとリスクがあります。
/* container 名 "card" が 500px より広いときだけ適用 */
@container card (min-width: 500px) {
.card-title {
font-size: 1.5rem;
}
}
/* 名前を指定しない → 最も近い container を query */
@container (min-width: 500px) {
.card-title {
font-size: 1.5rem;
}
}
3.3. 複数の container に同じ名前
同じ名前 を複数の container に付けられます — sidebar にも main にもある card すべてに同じルールを適用したいときに便利。1つの @container、複数の場所が反応。便利ですが、必要ないときは使いすぎないでください。
/* 両方とも名前 "card" */
.sidebar-card {
container: card / inline-size;
}
.main-card {
container: card / inline-size;
}
/* この query は両方の container に適用される */
@container card (max-width: 300px) {
.card-image {
display: none;
}
}
3.4. Name-only Container Query
CSS Containment Module Level 3 によると、名前だけ で query できます — サイズ条件は不要。「card という名前の container で囲まれているか?」ならスタイルを適用、というイメージです。
/* container 名のみで query */
@container card {
.card-content {
/* 名前 "card" の container で囲まれていれば適用 */
border-left: 4px solid blue;
}
}
注意: Name-only query は 2026年5月から Baseline Newly available(Chrome 148+、Firefox 151+、Safari 26.5+)。production では必ず動作確認を。
4. Style Query — サイズだけでなくスタイルで query する
Size query は「親は何 px 幅か?」に答えます。Style query はさらに「親は今どんな状態か?」 — dark mode、compact mode、design token... 本記事で一番好きなパートです。React context なしで、コンポーネントがコンテキストの「雰囲気」を読めるようになるから。
4.1. Style Query とは
Style Query は container の スタイル値(多くはカスタムプロパティ)を query できます。サイズだけではありません。
基本アイデア:container を定義し、その computed style に基づいて子孫に条件付きスタイルを適用する。
4.2. ポイント:すべての要素が Style Container
Size query は container-type の宣言が必要。Style query は... 何もいらない — すべての要素がデフォルトで style container。ブラウザが面倒を見てくれます。
すべての要素は style container — style を query するのに
container-nameやcontainer-typeは不要。
なぜ? Style query は layout loop を起こしません — 子のスタイルが親に逆流しない。size query とは違い、containment が必須です。
4.3. Style Query の構文
CSS Containment Module Level 3 の仕様では すべての computed style を query できます — ただし現行ブラウザが「本気で対応している」のはカスタムプロパティだけです(4.4 参照)。
/* カスタムプロパティの query — 対応済み */
@container style(--theme: dark) {
.card {
background: #1a1a2e;
color: #eee;
}
}
/* 論理演算子との組み合わせ */
@container style(--theme: dark) and (min-width: 400px) {
.card {
/* dark かつ幅 400px 超のとき */
}
}
仕様上の例 — production にはコピーしないで(広く未対応)
/* font-style の query — 仕様にはあるが、広く未対応 */
@container style(font-style: italic) {
em,
i,
q {
font-style: normal;
}
}
4.4. 現実:Style Query が対応しているのは Custom Properties のみ
仕様はあらゆる property-value ペアの query を夢見ています。現実のブラウザが対応しているのは Custom Properties — つまり CSS 変数だけ。それ以外は... 上の <details> を眺める程度に。
Chrome for Developers によると、仕様では font-weight: 800 のような query も可能ですが、実装は現時点で カスタムプロパティのみ です。
/* ✅ 有効 — カスタムプロパティの query(対応済み) */
@container style(--size: large) {
.text {
font-size: 2rem;
}
}
/* ❌ 無効(現時点) — プロパティを直接 query */
@container style(font-size: 20px) {
.text {
/* ほとんどのブラウザでは動かない */
}
}
朗報:カスタムプロパティ向け style query は 2026年5月から Baseline Newly available です。
4.5. 実例:コンテキストに応じた dark mode
各カードに className={isDark ? 'dark' : ''} を付ける代わりに、親 container に --theme をセット — 中のカードが自動でわかります。
/* container でテーマを宣言 */
.app {
--theme: dark;
}
/* テーマに基づく style query */
@container style(--theme: dark) {
.card {
background: #1e1e2e;
border-color: #444;
}
.card-title {
color: #fff;
}
}
@container style(--theme: light) {
.card {
background: #ffffff;
border-color: #ddd;
}
.card-title {
color: #222;
}
}
5. ブラウザ:レンダリングパイプラインにおける Container Query
第13回でレンダリングパイプラインは触れました。本記事に直結する部分だけおさらいします:
覚えておくこと: @container は container の layout 後 に評価されます — ブラウザは即座にスタイルを適用せず、再計算が必要な子孫にマークを付けます。だから resize 時にコンポーネントが一瞬「跳ねる」ことがある — バグではなく、ブラウザが正しく動いている(少しだけ遅い)証拠です。すべての <div> に container-type をシールのように貼る理由でもありません。
6. 実践:React + TypeScript で DashboardCard
第13回の ProfileCard は size query の入門でした。ここでは DashboardCard にアップグレード — named container、意味のある breakpoint、dark mode 用 style query。1コンポーネント、3コンテキスト、variant props なし。
6.1. 要件
シンプルなダッシュボードカード — でもどこに置いても生き残れること:
- 表示:title、value、icon、trend indicator
- container > 500px → 横レイアウト(icon 左、テキスト右)
- container 300–500px → 縦レイアウト、情報はすべて表示
- container < 300px → trend を非表示、フォント縮小(狭い sidebar に成長グラフは不要)
- 親に
--theme: dark→ 自動 dark mode
6.2. コード
import React from 'react';
import './DashboardCard.css';
import { TrendingUp, TrendingDown } from 'lucide-react';
interface DashboardCardProps {
title: string;
value: string | number;
icon?: React.ReactNode;
trend?: {
value: number;
direction: 'up' | 'down';
};
className?: string;
}
export const DashboardCard: React.FC<DashboardCardProps> = ({
title,
value,
icon,
trend,
className = '',
}) => {
return (
<div className={`dashboard-card-container ${className}`}>
<div className="dashboard-card">
{icon && <div className="card-icon">{icon}</div>}
<div className="card-content">
<span className="card-title">{title}</span>
<span className="card-value">{value}</span>
{trend && (
<span
className={`card-trend ${trend.direction}`}
aria-label={`${trend.direction === 'up' ? '増加' : '減少'} ${Math.abs(trend.value)}%`}
>
{trend.direction === 'up' ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
{Math.abs(trend.value)}%
</span>
)}
</div>
</div>
</div>
);
};
/* ============================================
STEP 1: デフォルトスタイル(古いブラウザ向けフォールバック)
============================================ */
.dashboard-card {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
background: #ffffff;
border-radius: 12px;
padding: 1.25rem 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
/* 重要: コンテンツの縮小を許可 */
min-width: 0;
}
.card-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #f0f4ff;
border-radius: 10px;
color: #4f46e5;
}
.card-content {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.card-title {
font-size: 0.8rem;
font-weight: 500;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card-value {
font-size: 1.5rem;
font-weight: 700;
color: #111827;
line-height: 1.2;
/* Production 向け: 長いテキスト — どちらか一方を選択 */
/* Strategy 1: 複数行に折り返し */
overflow-wrap: break-word;
word-break: break-word;
/* Strategy 2: 1行 + ellipsis(使う場合はコメント解除) */
/* white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; */
}
.card-trend {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
margin-top: 0.25rem;
}
.card-trend.up {
color: #10b981;
}
.card-trend.down {
color: #ef4444;
}
/* ============================================
STEP 2: @supports フォールバック付き Container Query
============================================ */
@supports (container-type: inline-size) {
.dashboard-card-container {
container-type: inline-size;
container-name: dashboard-card;
padding: 4px;
}
/* --- Query 1: 中サイズ container (300-500px) --- */
@container dashboard-card (max-width: 500px) and (min-width: 301px) {
.dashboard-card {
flex-direction: column;
text-align: center;
padding: 1rem;
}
.card-icon {
width: 56px;
height: 56px;
font-size: 1.75rem;
}
.card-value {
font-size: 1.25rem;
}
.card-title {
font-size: 0.7rem;
}
}
/* --- Query 2: 小さい container (< 300px) --- */
@container dashboard-card (max-width: 300px) {
.dashboard-card {
flex-direction: column;
text-align: center;
padding: 0.75rem;
gap: 0.5rem;
}
.card-icon {
width: 40px;
height: 40px;
font-size: 1.25rem;
}
.card-value {
font-size: 1rem;
}
.card-title {
font-size: 0.6rem;
}
/* 狭すぎるときは trend を非表示 */
.card-trend {
display: none;
}
}
/* --- Query 3: 広い container (> 500px) --- */
@container dashboard-card (min-width: 501px) {
.dashboard-card {
flex-direction: row;
padding: 1.25rem 1.5rem;
}
.card-value {
font-size: 1.75rem;
}
}
/* --- Query 4: Style Query — Dark Mode --- */
@container dashboard-card style(--theme: dark) {
.dashboard-card {
background: #1e1e2e;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.card-title {
color: #9ca3af;
}
.card-value {
color: #f3f4f6;
}
.card-icon {
background: #2d2d44;
color: #a5b4fc;
}
}
/* --- Query 5: Size + Style Query の組み合わせ --- */
@container dashboard-card style(--theme: dark) and (max-width: 300px) {
.dashboard-card {
background: #16162a;
border: 1px solid #2d2d44;
}
.card-value {
color: #e0e7ff;
}
}
}
6.3. コンポーネントの使い方(Production 向け)
Style query と継承: DarkThemeContainer が親に --theme: dark をセット。変数は .dashboard-card-container(container-name: dashboard-card)に 継承 されるので、カード側で再セットしなくても @container dashboard-card style(--theme: dark) が動きます。CSS 継承 — 古い機能ですが、style query と組み合わせると美味しいです。
App.tsx & App.css — 3コンテキストのデモレイアウト
import React from 'react';
import { DashboardCard } from './DashboardCard';
import { DollarSign, Users, BarChart } from 'lucide-react';
// inline style の代わりにレイアウトコンポーネント
const MainContent: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="main-content">{children}</div>;
};
const Sidebar: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="sidebar">{children}</div>;
};
// dark テーマの container
const DarkThemeContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div
className="dark-theme-container"
style={{ '--theme': 'dark' } as React.CSSProperties}
>
{children}
</div>
);
};
export const App: React.FC = () => {
return (
<div className="app">
{/* main content 内のカード(幅広) */}
<MainContent>
<h3>Main Content</h3>
<DashboardCard
title="売上"
value="12.5億"
icon={<DollarSign size={24} />}
trend={{ value: 15, direction: 'up' }}
/>
</MainContent>
{/* sidebar 内のカード(狭い) */}
<Sidebar>
<h3>Sidebar</h3>
<DashboardCard
title="アクセス数"
value="45.2K"
icon={<Users size={24} />}
trend={{ value: 8, direction: 'up' }}
/>
</Sidebar>
{/* dark mode container 内のカード */}
<DarkThemeContainer>
<h3 style={{ color: 'white' }}>Dark Sidebar</h3>
<DashboardCard
title="コンバージョン率"
value="3.2%"
icon={<BarChart size={24} />}
trend={{ value: 2, direction: 'down' }}
/>
</DarkThemeContainer>
</div>
);
};
.main-content {
width: 800px;
padding: 20px;
background: #f5f5f5;
}
.sidebar {
width: 250px;
padding: 20px;
background: #f5f5f5;
margin-top: 20px;
}
.dark-theme-container {
width: 400px;
padding: 20px;
background: #1a1a2e;
margin-top: 20px;
}
6.4. 結果
同じ DashboardCard、3つのコンテキスト — variant なし、場所ごとの media query もなし:
| コンテキスト | container 幅 | レイアウト | 備考 |
|---|---|---|---|
| Main content | 〜800px | 横並び、value 大 | Size query min-width: 501px
|
| Sidebar | 〜250px | 縦並び、フォント小 | trend indicator 非表示 |
| Dark container | 〜400px | 縦並び + dark theme | Style query --theme: dark
|
7. 実務で使えるパターン
よく使うパターンをいくつか — コピーして design system に合わせて breakpoint を調整してください。
7.1. Container Query による Responsive Grid
grid の列数が container に応じて変わる — viewport ではない。sidebar が狭ければ1列、main が広ければ4列 — 同じ grid コンポーネント。
.product-grid {
container-type: inline-size;
container-name: grid;
display: grid;
gap: 1rem;
}
/* container 幅に応じて列数を自動調整 */
@container grid (min-width: 800px) {
.product-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@container grid (min-width: 500px) and (max-width: 799px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@container grid (max-width: 499px) {
.product-grid {
grid-template-columns: 1fr;
}
}
7.2. Container に応じた Fluid Typography
cqi + clamp() — フォントが container にスケール、画面全体ではない。sidebar のカードがランディングページの hero と同じ文字サイズになることはありません。
.article-container {
container-type: inline-size;
}
@container (min-width: 600px) {
.article-title {
font-size: clamp(1.5rem, 4cqi, 3rem);
}
}
@container (max-width: 599px) {
.article-title {
font-size: clamp(1.2rem, 6cqi, 2rem);
}
}
7.3. ネストした Container Query
Container query は ネスト できます — outer が全体レイアウト、inner が内部を微調整。ただし深くネストしすぎると、パフォーマンスも頭もつらくなります。
.outer {
container: outer / inline-size;
}
.inner {
container: inner / inline-size;
}
/* outer container を query */
@container outer (min-width: 600px) {
.inner {
display: flex;
}
/* inner container を query(ネスト) */
@container inner (min-width: 300px) {
.inner-child {
flex: 1;
}
}
}
8. Container Query Units — cqi、cqb など
第13回で fluid layout の cqi に触れました。ここでは一覧表 — 第15回 で clamp() と viewport units を使った ページ全体 の fluid typography を深掘りします。本記事はコンポーネントレベルまで。
| 単位 | 意味 | 類似 |
|---|---|---|
cqi |
container の inline-size の1% |
vi(viewport inline) |
cqb |
container の block-size の1% |
vb(viewport block) |
cqw |
container の width の1% | vw |
cqh |
container の height の1% | vh |
cqmin |
cqi と cqb の小さい方 | vmin |
cqmax |
cqi と cqb の大きい方 | vmax |
vw/vhとの違い:cqi/cqbは query container 基準で、viewport ではありません。タイポグラフィと spacing がコンポーネント単位 — sidebar が hero セクションと同じフォントサイズになることはありません。
.card-title {
/* フォントサイズ = container 幅の 2〜4% */
font-size: clamp(1rem, 3cqi, 2.5rem);
}
.card-padding {
/* container に比例した padding */
padding: 2cqi;
}
9. Media Query、Container Query、Style Query — どれを使う?
Container Query を始めたばかりのときの定番質問。要約:それぞれ別の問いに答える — どれもがどれもを置き換えるわけではない。間違えるとネジで釘を打つようなもの — できるけど、長期的にはつらい。
| 状況 | 使うもの | 理由 |
|---|---|---|
| ページ全体の responsive(header、footer、メイングリッド) | Media Query | viewport が適切なコンテキスト |
| 再利用コンポーネント(card、sidebar、widget) | Container Query | 親 container に適応する必要がある |
| コンテキストに応じたテーマ・Design Token | Style Query | 親 container のスタイルを query |
| React のロジック変更(state、props) | JavaScript | CSS はロジックの代替にならない |
| Media Query | Container Query | Style Query | |
|---|---|---|---|
| query の基準 | Viewport | 親 container | container のスタイル |
| 適用先 | ページ全体 | 特定コンポーネント | コンテキスト別コンポーネント |
| コンポーネント再利用 | 難しい | 容易 | 非常に容易 |
| ブラウザサポート | 広い | Baseline(2026) | Baseline(2026) |
原則: Container Query は Media Query を 補完 する — 置き換えではない。コンポーネントが固定レイアウトにしか置かれないなら
@mediaの方がシンプル。over-engineer しないで。
10. パフォーマンス & Production のベストプラクティス
Container Query はタダではありません — container-type ごとにブラウザが maintain する context が増えます。container を resize → @container を再評価。アニメーションで resize が続く → ブラウザもあなたもつらい。
| 状況 | 影響度 |
|---|---|
| component boundary に数個の container | 無視できる |
すべての div に container-type
|
多数の context、遅い |
| container の連続 resize(アニメーション) | 連続再評価 |
| 多数の container を持つ大きな DOM | context 維持で CPU 消費 |
黄金律: container-type は component boundary — 最外側の wrapper だけに置く。すべての <div> を query container にしないで。DOM はクリスマスツリーではありません。
10.1. @supports によるフォールバック
/* 古いブラウザ向けフォールバック */
.dashboard-card {
/* デフォルトスタイル */
}
/* 対応ブラウザ向け */
@supports (container-type: inline-size) {
.dashboard-card-container {
container-type: inline-size;
}
@container (max-width: 500px) {
.dashboard-card {
/* responsive スタイル */
}
}
}
10.2. 意味のある breakpoint
387px という breakpoint は、特定の画面で trial-and-error した結果であることが多い — 半年後誰も理由を覚えていません。意味のある名前を:compact、medium、expanded。
/* ✅ 良い — 意味のある breakpoint */
@container (max-width: 300px) {
/* compact */
}
@container (min-width: 301px) and (max-width: 500px) {
/* medium */
}
@container (min-width: 501px) {
/* expanded */
}
/* ❌ 悪い — 謎の数字 */
@container (max-width: 387px) {
}
@container (min-width: 423px) {
}
10.3. Media Query と Container Query の併用
/* ページ全体レイアウト用 Media Query */
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
/* 内部コンポーネント用 Container Query */
@container dashboard (max-width: 300px) {
.card-trend {
display: none;
}
}
10.4. 意味のある container 名
/* ✅ 良い */
.sidebar {
container-name: sidebar;
}
.card-grid {
container-name: card-grid;
}
.widget-area {
container-name: widget;
}
/* ❌ 悪い */
.container-1 {
container-name: c1;
}
.wrapper {
container-name: w;
}
10.5. contain プロパティを理解する
container-type は対応する containment を自動で有効化 — inline-size ≈ contain: layout inline-size、size ≈ contain: layout size。Container Query は CSS Containment エコシステムの一部で、単独の浮いた機能ではありません。
11. チェックリスト:Container Query を諦める前に
query が動かない? ブラウザのせいにする前にこのチェックリスト — 90% は container-type の書き忘れか container 名のミスです。
-
container-typeを宣言したか?
Size query にはcontainer-type: inline-sizeかsizeが必要。忘れると query は黙る — CSS はエラーを出さず、ただ何もしません。 -
container 名は正しいか?
@container sidebar (...)なのにcontainer-name: sidebarがない → 動きません。 -
Style Query は Custom Property を使っているか?
現状はカスタムプロパティのみ。@container style(font-style: italic)を仕様から production にコピーしないで。 -
@supportsでフォールバックを用意したか?
古いブラウザはまだいる — デフォルトスタイルだけで十分使えること。 -
overflow: hiddenの影響はないか?
container のサイズが変わり → query の結果が期待と違うことがある。 -
ブラウザサポートを確認したか?
Size + style query(カスタムプロパティ)は 2026年5月から Baseline Newly available — それでも ship 前に MDN や Can I Use を確認を。 -
構文は正しいか?
/* 正しい */ @container (min-width: 400px) { } @container sidebar (min-width: 400px) { } @container style(--theme: dark) { } /* 間違い */ @container sidebar min-width: 400px { } /* 括弧がない */ -
layout loop は起きないか?
Container query は containment により loop しません。Style query も同様 — 安心してください、今回はあなたのせいではない(たぶん)。
12. まとめと参考資料
12.1. 要点
本記事は第13回を、production でよく使う3つで補完します:
- Container Naming — DOM がネストしているとき正しい container を query
- Style Query — カスタムプロパティによる responsive(テーマ、トークン)
-
DashboardCard — size + style query、
@supportsフォールバック付き
12.2. 3つのツール — 1つのエコシステム
Media Query → ページは広いか狭いか?
Container Query → コンポーネントはいくつ分の場所があるか?
Style Query → コンテキストのテーマは何か?
詳細な比較表は セクション9 へ。
12.3. 参考資料
- MDN: CSS container queries
- MDN: @container アットルール
- web.dev: Container queries
- web.dev: New to the web platform in May 2026
- web.dev: May 2026 Baseline monthly digest
- W3C CSS Containment Module Level 3
おわりに
「768px の画面でカードはどう見えるか」と聞くのはやめましょう。「container が 300px のとき、カードに何が必要か」— そして「親が dark のとき、カードは自分でわかるか」と聞く。
それが、コンポーネントが本当に再利用できる Design System の作り方 — 1つのコードベース、あらゆるコンテキスト。魔法ではなく、正しい問いをしているだけです。
👉 次回
【Frontend CSS – パート15】Fluid Typography完全ガイド — clamp()・viewport単位・スケーラブルな文字サイズ設計

