ご注意
この記事は AI のサポートを受けていますが、
1. 課題:Media Query ではコンポーネントを再利用できない
こんな経験、ありませんか?
-
ケース1:
ProductCardを作った — トップページでは完璧。デザイナーが「sidebar 280px に入れて」と言う。はみ出す、文字が重なる、画像が歪む。DevTools を開いて@media (max-width: 768px)を追加 — OK。modal に入れる — また崩れる。max-width: 480pxを追加。4列 grid に入れる — また崩れる。同じコンポーネントを無数のコンテキスト向けに調整していて、media query は CSS を膨らませるだけで根本原因を解決していない。 -
ケース2: ナビバー — 大画面ではロゴ左・メニュー中央・アバター右、小画面ではハンバーガー。breakpoint
768pxを書いて安心。翌朝上司から「この navbar を管理画面の sidebar に入れて — sidebar は 300px だけど」。media query は sidebar の幅を知らない。知っているのは viewport だけ。
要点: media query は ページレベル の responsive に優れたツールです — 全体レイアウトの切り替え、サイト全体の padding 調整、マウス付きデバイスの hover 処理など。どこにでも置けるコンポーネントシステム を作るときは、再利用の障壁になりがちです。
モダンな responsive は「画面サイズに合わせて設計する」ことではなく、「コンポーネントが実際に受け取るスペースに合わせて設計する」ことです。
2. 本質:モダンな responsive = 実際のスペースに基づく
Responsive Web Design(RWD)— Ethan Marcotte が 2010 年頃に提唱 — は Web の作り方を変えました。media query が柱でした。でも Web は変わりました。もはや「ページ」だけではなく、sidebar・modal・grid、どこにでも現れる コンポーネントシステム を作っています。
Jen Simmons は新しい方向性を Intrinsic Web Design と呼びます — 固定 breakpoint で外側から制御するのではなく、コンテンツとコンテキストに基づいて設計する。
要するに:「画面は何 px か」と聞くのではなく、「このコンポーネントは今いくつ分の場所があるか」と聞く。シンプルに聞こえますが、CSS の書き方が根本から変わります。
3. ブラウザ:レンダリングパイプラインにおける Container Query
responsive がレンダリングのどこに位置するか — そしてコンポーネントがなぜ一瞬レイアウトが「跳ねる」か — を理解するため、第1回から紹介してきたパイプラインと、Container Query が評価されるタイミングをまとめます。
実際の順序:
- ブラウザが DOM と CSSOM を作り、Render Tree に結合
- formatting context(Block / Flex / Grid)、available space、intrinsic size、extrinsic constraints を確定
- Layout pass 1 — container を含む基本サイズを計算
-
container を計測(
inline-size、block-size)→ Container Query を評価 - 新しいスタイルでサイズが変われば Layout pass 2
- Paint と Composite
Container Query は最初から走りません — ブラウザは先に container の幅を知る必要があります。追加の layout パスが発生することはありますが、パイプライン外のハックではありません。
この順序を理解すると debug に役立ちます。レイアウトが「跳ねる」のは、query が発火してサイズを変えるスタイルが適用されたからで、ランダムなバグではないことが多いです。ブラウザには最適化もあり、常に pass 2 が必要なわけではありません。
4. Media Query — 「粗い」ツールの盲点
4.1. Media Query が知っているのは viewport だけ
media query が知っているのは 2つだけ です:
- viewport のサイズ
- ブラウザのデフォルト font size(
:rootに設定したfont-sizeではない)
html {
font-size: 32px;
}
@media (min-width: 35rem) {
body {
background: lightseagreen;
}
}
質問: 背景色が変わるのは viewport が何 px のとき?
1120px(35 × 32)と思ったら — それは間違いです。
media query はあなたが設定した font-size を見ません。ブラウザのデフォルト font size — 通常 16px — を見ます。実際の breakpoint:35 × 16 = 560px。私もここを勘違いして半日 debug したことがあります。
min-width / max-width 条件内の rem / em は、:root の font-size ではなくブラウザの initial font-size(通常 16px)に基づきます。breakpoint = rem の値 × 16。
4.2. Viewport ≠ Available Space
ここが media query が最も「盲目」なところです。
Viewport(画面)
────────────────────────────────────────────────────
│ │
│ Sidebar (280px) Content (900px) │
│ ██████████████ ████████████████████ │
│ │
│ カードの実際の幅: 280px │
│ Media Query が見る: 1180px │
└─────────────────────────────────────────────────────┘
media query は viewport = 1180px → 「大画面」スタイルを適用。sidebar 内のカードは 280px しかない。レイアウトが崩れる — 新しい breakpoint を足す — また別のコンテキストで崩れる。おなじみのループです。
4.3. Media Query が正しい選択肢のとき
| ユースケース | 例 |
|---|---|
| ページレベルレイアウト | 2列から1列への切り替え |
| グローバル調整 | サイト全体の padding 増加 |
| デバイス固有 |
@media (hover: hover) でマウス付きデバイス |
| グローバル typography | ページ全体の font-size 調整 |
複数コンテキストで再利用するコンポーネント → Container Query の方が適していることが多い。 media query が悪いわけではなく、使う場所が違うだけです。
5. Container Query — 本当の component-first
5.1. Container Query とは?
Container query は、viewport ではなく 親 container のサイズ に基づいて要素にスタイルを当てられます。同じコンポーネントでも、container が違えばレイアウトが違う。viewport を聞く必要はありません。
5.2. 基本構文
/* ステップ1: container を定義 */
.card-container {
container-type: inline-size; /* 幅のみ追跡 */
}
/* ステップ2: query */
@container (inline-size > 400px) {
.card {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@container (inline-size <= 300px) {
.card {
padding: 0.5rem;
}
}
2ステップ:container を宣言し、query する。シンプルです — 難しいのは、どの要素を container にするかの選択です。
5.3. Container Query 単位 — cqi、cqb
| 単位 | 意味 |
|---|---|
cqi |
container の inline size の 1%(通常は width) |
cqb |
container の block size の 1%(通常は height) |
.card {
font-size: max(2cqi, 14px);
padding: 3cqi;
}
typography と spacing が container に比例 — サイズごとに breakpoint を書く必要がありません。
6. Intrinsic Sizing & Auto-fit Grid — responsive の補助
第11回 で min-content、max-content、fit-content とブラウザの intrinsic size 計算を詳しく扱いました。本記事では Container Query と組み合わせやすい2つのツール だけ — 不要な media query を避けるために。
6.1. コンポーネントを「ぴったり」にしたいときの fit-content
.card-title {
width: fit-content;
max-width: 100%;
}
コンテンツに合わせて広がるが、親 container からはみ出さない — 狭い sidebar 内のカードに便利です。
6.2. Intrinsic Grid — 親のスペースに応じた responsive
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
-
auto-fit:利用可能なスペースに応じて列を自動生成 -
minmax(250px, 1fr):各列は最低 250px - media query 不要 — container が狭ければ列が減り、広ければ増える
親レイアウトレベルでの「タダで使える」responsive です。intrinsic sizing を根本から理解したい場合は 第11回 に戻ってください。
7. Fluid Layout — breakpoint なしの responsive
7.1. clamp() — 賢い制限
/* ページレベル: viewport に比例 */
.page-title {
font-size: clamp(1rem, 2.5vw, 2rem);
}
/* コンポーネントレベル: container に比例(再利用カード向け) */
.card-title {
font-size: max(3cqi, 1rem);
}
vw / % は ページ全体 の typography に向いています — viewport や extrinsic な親要素を参照するからです。sidebar 280px にも main 900px にも置けるコンポーネントでは、cqi の方が vw より安全 — 文字は画面ではなく実際の container に比例します。
7.2. container 単位による Fluid Typography
.card {
container-type: inline-size;
}
.card-title {
font-size: max(3cqi, 1rem);
}
文字サイズは container に応じて変わるが、1rem 未満にはならない — どのサイズでも読みやすく保てます。
8. ResizeObserver → Container Query:CSS の進化
Container Query 以前は、同様のことを JavaScript でやる必要がありました — しばしば暫定のハックとして。私も ResizeObserver を使ったコンポーネントをメンテしていたことがあります — unmount のたびに disconnect() を忘れないこと、連続 resize 時の race condition。Container Query はその手間から解放してくれます。
新しい方法 — Container Query:
@container (inline-size > 400px) {
.card {
flex-direction: row;
}
}
旧来の方法 — ResizeObserver + class 切り替え(参考)
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
const width = entry.contentRect.width;
if (width > 400) {
card.classList.add("wide");
} else {
card.classList.remove("wide");
}
}
});
observer.observe(container);
.card.wide {
flex-direction: row;
}
比較:
| ResizeObserver + JS | Container Query | |
|---|---|---|
| Performance | オーバーヘッド(JS 実行) | ネイティブ(ブラウザ最適化) |
| Complexity | 複雑(observer 管理) | シンプル(宣言的) |
| Maintenance | 難しい(JS + CSS 分離) | 楽(すべて CSS 内) |
9. Decision Tree — ツール選びのフローチャート
同じカードに5つ目の @media を書く前に、このフローチャートで確認してください:
印刷してモニター横に貼ってもいいくらいです — 感覚で breakpoint を足すよりマシです。
10. React TypeScript 実践
Container Query を使った ProfileCard — 同じコンポーネントを sidebar、main、grid に置いても自動適応します。
ProfileCard.tsx
import React from 'react';
import './ProfileCard.css';
interface ProfileCardProps {
name: string;
role: string;
avatar: string;
bio: string;
stats: { label: string; value: string }[];
}
export const ProfileCard: React.FC<ProfileCardProps> = ({
name,
role,
avatar,
bio,
stats,
}) => {
return (
<div className="profile-card-container">
<div className="profile-card">
<div className="profile-card-header">
<img className="profile-card-avatar" src={avatar} alt={name} />
<div className="profile-card-info">
<h3 className="profile-card-name">{name}</h3>
<p className="profile-card-role">{role}</p>
</div>
</div>
<p className="profile-card-bio">{bio}</p>
<div className="profile-card-stats">
{stats.map((stat, i) => (
<div key={i} className="profile-card-stat">
<span className="profile-card-stat-value">{stat.value}</span>
<span className="profile-card-stat-label">{stat.label}</span>
</div>
))}
</div>
</div>
</div>
);
};
ProfileCard.css — Container Query(クリックで展開)
.profile-card-container {
container-type: inline-size;
}
.profile-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.profile-card-header {
display: flex;
align-items: center;
gap: 1rem;
}
.profile-card-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
}
.profile-card-name {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
}
.profile-card-role {
margin: 0;
font-size: 0.875rem;
color: #666;
}
.profile-card-bio {
margin: 0;
font-size: 0.875rem;
color: #444;
line-height: 1.5;
}
.profile-card-stats {
display: flex;
gap: 1.5rem;
border-top: 1px solid #eee;
padding-top: 1rem;
}
.profile-card-stat {
display: flex;
flex-direction: column;
align-items: center;
}
.profile-card-stat-value {
font-weight: 700;
font-size: 1.1rem;
}
.profile-card-stat-label {
font-size: 0.75rem;
color: #888;
}
@container (inline-size > 400px) {
.profile-card {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.profile-card-header {
flex: 0 0 100%;
}
.profile-card-bio {
flex: 1;
min-width: 200px;
}
.profile-card-stats {
flex: 0 0 auto;
border-top: none;
padding-top: 0;
border-left: 1px solid #eee;
padding-left: 1.5rem;
}
}
@container (inline-size > 600px) {
.profile-card-avatar {
width: 80px;
height: 80px;
}
.profile-card-name {
font-size: 1.3rem;
}
}
異なるコンテキストでの使用
import React from 'react';
import { ProfileCard } from './ProfileCard';
import './Dashboard.css';
const user = {
name: '田中 太郎',
role: 'Senior Frontend Developer',
avatar: '/avatar.jpg',
bio: 'Building responsive and accessible web applications with React and CSS.',
stats: [
{ label: 'Projects', value: '42' },
{ label: 'Followers', value: '1.2K' },
{ label: 'Posts', value: '89' },
],
};
export const Dashboard = () => (
<div className="dashboard">
<aside className="sidebar">
<ProfileCard {...user} />
</aside>
<main className="main">
<ProfileCard {...user} />
</main>
<section className="grid">
{[1,2,3,4].map(i => <ProfileCard key={i} {...user} />)}
</section>
</div>
);
.sidebar { width: 280px; }
.main { flex: 1; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
ProfileCard には media query が1つもありません。 container に応じて自動適応 — sidebar 280px でも main が広くても問題なし。
2枚目の図(任意): 2つのコンテキストをクロップ — sidebar(縦レイアウト、stats が横並び)と main(横レイアウト、stats が右側)。公開前に IMAGE_URL_LAYOUT_COMPARE を差し替えてください。
11. ブラウザサポート & フォールバック戦略
11.1. ブラウザサポート
Container Query は以下からサポートされています:
- Chrome 105+(2022年8月)
- Firefox 110+(2023年2月)
- Safari 16.0+(2022年9月)
- Edge 105+
Baseline "Newly available" 2023 によると、多くの新規プロジェクトでは大きな心配なく使えます。レガシープロジェクトにはフォールバックを用意しましょう。
11.2. フォールバック戦略
本番で安全に — @supports を使います:
/* フォールバック: 古いブラウザ向け media query */
.profile-card {
flex-direction: column;
}
@media (min-width: 600px) {
.profile-card {
flex-direction: row;
}
}
/* Container Query 対応ブラウザ → 上書き */
@supports (container-type: inline-size) {
.profile-card-container {
container-type: inline-size;
}
@container (inline-size > 400px) {
.profile-card {
flex-direction: row;
}
}
}
プログレッシブエンハンスメント:フォールバックが先に動き、対応ブラウザでは Container Query が上書きします。
media query によるフォールバックは 近似にすぎません — 依然として viewport を見ており、カードが sidebar 280px 内にあるのか main 900px 内にあるのかは分かりません。古いブラウザ向けに使い、すべてのコンテキストで Container Query の代替にはなりません。
12. media query を追加する前のチェックリスト
もう一度 @media を書く前に、このチェックリストを確認してください:
-
コンポーネントの問題か、ページレイアウトの問題か?
- コンポーネント → Container Query
- ページレイアウト → Media Query
-
intrinsic sizing で解決できないか?(
fit-content— 詳細は第11回) -
clamp()/cqiで滑らかにスケールできないか? -
親要素に
container-typeを宣言したか? - ブラウザサポートを確認し、フォールバックを用意したか?
-
Container Query のパフォーマンスを考慮したか?
- 追加の layout pass が発生する可能性 — 深くネストしすぎない
13. まとめ — 情報を与えて、ブラウザに決めさせる
ブラウザには強力なレイアウトアルゴリズムがあります。モダンな responsive はブラウザを自分の意図に無理やり合わせることではなく、最適な解を選べるだけの情報を与えることです。
- Media Query は viewport を見る — Container Query は親 container を見る
- コンポーネントは自分自身の responsive を管理すべき
- Intrinsic sizing + auto-fit grid で breakpoint なしの適応が可能(第11回)
-
cqiとclamp()で滑らかな体験 — コンポーネントはcqi、ページはvwを優先 - Container Query は追加の layout pass を招く可能性 — 意図を持って使い、深いネストは避ける
14. 未来 — Responsive Web Design はどこへ向かうか
近い将来のトレンド:
-
Anchor Positioning(2024+):他の要素を基準に配置 — popover・tooltip が
position: absolute+ 手計算に依存しなくなる - Style Query(開発中):container の CSS カスタムプロパティ値で query — サイズだけでなく 状態 に応じた responsive
- 将来の CSS は viewport 依存が減り、実際のコンテキスト依存が増える
viewport は依然として重要 — ただし唯一の中心ではなくなりつつあります。
参考資料:
- MDN: CSS Container Queries
- MDN: Using Container Size and Style Queries
- web.dev: Container Queries
- web.dev: Baseline
- CSS-Tricks: Container Query Units cqi and cqb
- This Dot Labs: CSS Container Queries, what are they?
- W3C CSS Containment Module Level 3
- CSS Anchor Positioning

