4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Frontend CSS – パート13】ブラウザから見たレスポンシブレイアウト:なぜMedia Queryだけでは不十分なのか?

4
Posted at

image.png

ご注意
この記事は 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 が評価されるタイミングをまとめます。

実際の順序:

  1. ブラウザが DOM と CSSOM を作り、Render Tree に結合
  2. formatting context(Block / Flex / Grid)、available space、intrinsic size、extrinsic constraints を確定
  3. Layout pass 1 — container を含む基本サイズを計算
  4. container を計測inline-sizeblock-size)→ Container Query を評価
  5. 新しいスタイルでサイズが変われば Layout pass 2
  6. 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 単位 — cqicqb

単位 意味
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-contentmax-contentfit-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

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(クリックで展開)
ProfileCard.css
.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;
  }
}

異なるコンテキストでの使用

Dashboard.tsx
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>
);
Dashboard.css
.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 が広くても問題なし。

image.png

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 はブラウザを自分の意図に無理やり合わせることではなく、最適な解を選べるだけの情報を与えることです。

  1. Media Query は viewport を見る — Container Query は親 container を見る
  2. コンポーネントは自分自身の responsive を管理すべき
  3. Intrinsic sizing + auto-fit grid で breakpoint なしの適応が可能(第11回)
  4. cqiclamp() で滑らかな体験 — コンポーネントは cqi、ページは vw を優先
  5. Container Query は追加の layout pass を招く可能性 — 意図を持って使い、深いネストは避ける

14. 未来 — Responsive Web Design はどこへ向かうか

近い将来のトレンド:

  • Anchor Positioning(2024+):他の要素を基準に配置 — popover・tooltip が position: absolute + 手計算に依存しなくなる
  • Style Query(開発中):container の CSS カスタムプロパティ値で query — サイズだけでなく 状態 に応じた responsive
  • 将来の CSS は viewport 依存が減り、実際のコンテキスト依存が増える

viewport は依然として重要 — ただし唯一の中心ではなくなりつつあります。


参考資料:


👉 次回

【Frontend CSS – パート14】Style Query・Container Naming・実践パターンまで徹底解説

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?