レスポンシブデザインとは、1つのURL・1つのHTMLに対してCSSで表示を切り替え、画面幅にかかわらず同じコンテンツを最適なレイアウトで見せる設計手法です。実装で押さえる要点は3つで、viewportメタタグを入れること、min-widthの単方向でモバイルファーストに書くこと、そしてbox-sizing: border-boxでボックスモデルを統一することです。
この記事では、基本の3点に加えて、コンポーネント単位で切り替えるコンテナクエリ(@container)と、モバイルの100vh問題を解くdvh / svh / lvhまで扱います。最後に「書いたCSSが本当に崩れていないか」をブラウザのコンソールで数値判定する手順を置いたので、実装したらそのまま合否を確認できます。
レスポンシブデザインとは何か
レスポンシブデザインは、ウェブページが表示領域の幅に応じてレイアウトを自動的に組み替えるデザイン手法です。ここで重要なのは、これが「スマートフォン専用のURLを別に用意する」手法ではないという点です。URLもHTMLも1つのまま、CSSだけで見え方を変えます。そのためコンテンツが二重に存在せず、更新も1か所で済みます。
レスポンシブデザインを構成する3つの要素
- 流動的なレイアウト — 幅を固定pxではなく
%/fr/min()などの相対値で指定し、画面幅に合わせて伸縮させる - 条件によるCSSの切り替え — 画面幅を条件にする
@media、親コンテナ幅を条件にする@containerの2種類がある - モバイルファースト — 狭い画面向けのCSSをベースにし、広い画面だけを
min-widthで上書きする
この3つのうち、初学者がつまずくのは1つ目と3つ目です。幅の指定を相対値にしただけでは横スクロールは消えず、切り替えの向きを揃えないとCSSがすぐ読めなくなります。以降のセクションでは、その2点を実際のコードと数値で潰していきます。デザインカンプの段階で決めておくべきことはWebデザインの基本ルールにまとめています。
viewportメタタグを入れる(すべての起点)
レスポンシブデザインを機能させるには、HTMLのhead内にviewportメタタグを記述する必要があります。このタグがないと、スマートフォンのブラウザはページを幅980px程度の仮想的なウィンドウとして描画し、全体を縮小表示します。メディアクエリを何本書いてもモバイル向けの分岐が発火しないので、まずここを確認します。
<meta name="viewport" content="width=device-width, initial-scale=1.0">- width=device-width — ビューポートの幅を端末の画面幅に合わせる
- initial-scale=1.0 — 読み込み時のズーム倍率を100%にする
ここに user-scalable=no や maximum-scale=1.0 を足してピンチズームを禁止しないでください。WCAG 2.2 の達成基準1.4.4「テキストのサイズ変更」は、支援技術なしでテキストを200%まで拡大でき、かつ内容と機能が失われないことをレベルAAで求めています(W3C WAI: Understanding SC 1.4.4 Resize Text)。ズームを禁止すると、拡大しないと読めない利用者がページから締め出されます。
モバイルファーストは min-width の単方向で書く
モバイルファーストとは、メディアクエリの外側に狭い画面向けのCSSを書き、広い画面だけを min-width で上書きする書き方です。ポイントは上書きの向きを一方向に固定することにあります。max-width と min-width を両方使って768pxと769pxで二重に分岐させる書き方は、モバイルファーストではありません。両側から条件が飛んでくるため、どちらの宣言が勝つかを毎回追う必要が出てきます。
/* ベース = 狭い画面。メディアクエリの外に書く */
.container {
width: 100%;
padding: 20px;
}
/* 広い画面だけを上書きする。max-width の分岐は書かない */
@media (min-width: 768px) {
.container {
width: min(80%, 1080px);
margin-inline: auto;
}
}この書き方の利点は、ベースCSSが最も単純な1カラムのレイアウトになり、分岐が「広い画面のときだけ足す」の一方向に限られることです。なお「モバイル向けにすると読み込みが速くなる」といった効果は画像やスクリプトの量に左右されるため、CSSの書き方だけでは断定できません。速度はPageSpeed Insightsの見方で実測して判断してください。
メディアクエリそのものの構文(メディアタイプ、論理演算子、指定できるメディア特性の一覧)はメディアクエリの基本と使い方で解説しています。また @media (width >= 768px) のように比較演算子で書くrange構文も使えます(Baseline: widely available に2025年9月27日到達/webstatus.dev)。詳しくはメディアクエリの新しい記法を参照してください。
ブレークポイントは「崩れる地点」で決める
ブレークポイントとは、CSSの適用を切り替える画面幅の境界値です。端末名から逆算するのではなく、自分のコンテンツを縮めていって最初に読みづらくなった幅を境界にします。端末の画面サイズは毎年変わりますが、「ナビが1行に収まらない」「本文の1行が長すぎる」といった崩れ方はコンテンツ固有なので、後から見ても理由が分かる基準になります。
| メディアクエリ | レイアウトが崩れる典型的な地点 | そこで何が変わるか |
|---|---|---|
| ベースCSS(クエリなし) | —(ここが基準) | すべて1カラムで縦積み。画像は幅いっぱい |
| @media (min-width: 480px) | カードを2枚横に並べると1枚が200px を切り、見出しが2行に折れる | カードリストを2列に。フォームのラベルと入力欄を横並びに |
| @media (min-width: 768px) | グローバルナビの項目が1行に収まらず折り返す | ハンバーガーメニューを展開型ナビへ。本文とサブ情報を2カラムに |
| @media (min-width: 1024px) | 本文の1行が全角50字を超えて視線の戻り先を見失う | コンテンツ幅に上限を設ける。サイドバーを常時表示に |
| @media (min-width: 1280px) | 左右の余白が広がりすぎて要素同士の関連が読み取れない | 最大幅を固定して中央寄せ。グリッドの列数を増やす |
実務では768pxと1024pxの2本だけで足りるケースが多く、必要になった時点で足していくのが効率的です。ブレークポイントを増やすほど、後述の数値チェックで確認すべき幅の組み合わせも増えます。段組みそのものの切り替えはCSS Gridを使ったレイアウトで組むと記述量を抑えられます。
崩れないベースCSS(box-sizing と流体タイポグラフィ)
スマートフォンで横スクロールが出る原因として最も多いのは、width: 100% と padding の併用です。ブラウザ既定の box-sizing: content-box のままだと、指定した幅の外側にpaddingとborderが加算されるため、100%を指定した要素が親より広くなります。
なぜ375pxの画面が415pxになるのか
width: 100% は「親要素の幅と同じ」を意味します。ここに padding: 20px を足すと、content-box では左右あわせて40pxが幅の外に足され、要素の実寸は「親の幅 + 40px」になります。幅375pxの端末なら415pxです。
/* NG: 375px の画面で要素の実寸が 415px になる */
.container {
width: 100%;
padding: 20px; /* 左右20pxが width の外側に加算される */
}このCSSだけを読み込んだページを幅375pxで開き、後述のスクリプトで測ると scrollWidth 415 / clientWidth 375 / はみ出し 40px になります。逆に言えば、この40pxは box-sizing を切り替えるだけで消えます。border-box はpaddingとborderを幅の内側に含める値です(MDN: box-sizing)。
そのまま貼れるベースCSS
以下は、ここまでの内容をまとめたベースCSSです。擬似要素まで含めて border-box に統一し、ベースをモバイル、上書きを min-width の一方向に固定しています。
/* 1) 擬似要素まで含めてボックスモデルを統一する */
*, *::before, *::after {
box-sizing: border-box;
}
/* 2) ベース = モバイル。メディアクエリの外に書く */
body {
margin: 0;
font-size: 1rem; /* 16px。本文はこれより小さくしない */
line-height: 1.7;
}
.container {
width: 100%;
padding: 20px; /* border-box なので 100% を超えない */
}
/* 3) 広い画面だけを min-width で上書きする */
@media (min-width: 768px) {
.container {
width: min(80%, 1080px);
margin-inline: auto;
}
}
/* 4) 画像は親からはみ出させない */
img {
max-width: 100%;
height: auto;
}このCSSは実際にブラウザで読み込み、幅375px / 390px / 768px / 1280pxの4点すべてで scrollWidth と clientWidth が一致すること(はみ出し0px)を確認しています。測り方はこの記事の後半にあります。
文字サイズは clamp() で流体化する
clamp(最小値, 推奨値, 最大値) を使うと、ブレークポイントごとに font-size を書き直さずに文字サイズを連続的に変化させられます。ただし書き方には注意点があります。MDNは、テキストサイズの制御に clamp() を使うとき最大値を相対単位にし、最小値の2倍以上にすることを求めています。ページを200%までズームできるようにするためです(MDN: clamp() — Accessibility)。あわせて、最小値そのものも本文なら 1rem(=16px)を下回らせないようにします。
/* NG: 最大値が絶対単位(px)で、しかも最小値の2倍に届いていない */
.lead { font-size: clamp(1rem, 2vw, 20px); }
/* OK: 最大値は相対単位、かつ最小値の2倍。最小値も 1rem = 16px を下回らない */
.lead { font-size: clamp(1rem, 0.95rem + 0.5vw, 2rem); }推奨値を 0.95rem + 0.5vw のように「rem + vw」で書くのは、vw だけにするとズーム時に文字が拡大されなくなるためです。remの項を残しておくと、ユーザーのフォント設定にも追随します。本文のフォント指定全般はCSSフォント指定ガイド、個々のプロパティの意味はCSSプロパティ一覧にまとめています。
画像・タップ領域・ユーザー設定への対応
レイアウトが組めたら、次は中身の要素をモバイルに合わせます。ここで混同されやすいのが画像の出し分けです。目的が2種類あり、使うHTMLも変わります。
srcset は解像度切り替え、picture はアートディレクション
同じ絵柄のまま、画面や端末に合ったサイズのファイルを選ばせたい場合は img の srcset と sizes を使います。ブラウザが画面幅と画素密度から最適な1枚を選びます。width と height を必ず書いて縦横比を先に伝え、読み込み前後でレイアウトがずれる(CLS)のを防ぎます。
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(min-width: 768px) 720px, 100vw"
width="1600" height="900"
loading="lazy" decoding="async"
alt="オフィスで打ち合わせをする2人">いっぽう 画面幅によって絵柄そのものを変えたい場合は picture と source の media 属性を使います。横長の写真をスマホでは人物中心にトリミングした別ファイルへ差し替える、といった用途です。これをアートディレクションと呼びます(MDN: picture要素)。
<picture>
<source media="(min-width: 768px)" srcset="hero-wide.jpg">
<img src="hero-square.jpg" width="800" height="800"
alt="製品を手に取る利用者">
</picture>言い換えると、srcset と sizes は「どのサイズのファイルを落とすか」をブラウザに任せる仕組み、picture media は「どの絵柄を出すか」を作り手が指示する仕組みです。書き分けの詳細はpictureタグによる画像の切り替えで解説しています。
タップ領域はWCAGの等級で書き分ける
「タップ領域は最小44px」という数字がよく引用されますが、これはWCAG 2.2ではレベルAAAの基準です。等級ごとに数値が違うので、目標に合わせて使い分けます。
| 達成基準 | 等級 | ターゲットの大きさ |
|---|---|---|
| SC 2.5.8 Target Size (Minimum) | レベルAA | 24 × 24 CSSピクセル以上(間隔が確保できる場合など例外あり) |
| SC 2.5.5 Target Size (Enhanced) | レベルAAA | 44 × 44 CSSピクセル以上 |
出典はW3C WAI: Understanding SC 2.5.8 Target Size (Minimum)とW3C WAI: Understanding SC 2.5.5 Target Size (Enhanced)です。多くのサイトはAAを目標にするため必須ラインは24×24ですが、指で操作する前提のボタンは44×44を確保しておくと誤タップが減ります。
.btn {
min-width: 44px; /* レベルAAAの 44 × 44 を満たす */
min-height: 44px;
padding: 12px 20px;
font-size: 1rem;
}prefers系のメディア特性に応答する
メディアクエリは画面幅だけでなく、OS側のユーザー設定も条件にできます。最低限、動きの抑制とカラースキームの2つには応答しておきます(MDN: prefers-reduced-motion)。
/* 動きを減らす設定をしている利用者にはアニメーションを止める */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* ダークテーマ設定に応答する */
@media (prefers-color-scheme: dark) {
:root { color-scheme: dark; }
body {
background: #14181f;
color: #e8eaed;
}
}color-scheme: dark を併記しておくと、フォーム部品やスクロールバーなどブラウザが描画するUIもダーク側の配色になります。ここで紹介した以外の細かい表現技法はCSSテクニック集にまとめています。
コンポーネント基準で切り替えるコンテナクエリ
コンテナクエリ(@container)とは、ビューポート幅ではなく親コンテナの幅を条件にCSSを切り替える仕組みです。Baseline: widely available に2025年8月14日に到達しており、newly available は2023年2月14日、対応バージョンはChrome 105 / Safari 16 / Firefox 110 です(webstatus.dev: Container queries)。
メディアクエリでは解けない問題があります。同じカードコンポーネントを、広い本文カラムとサイドバーの両方に置く場合です。ビューポートが1280pxでも、サイドバーの中では幅が280pxしかありません。メディアクエリはビューポート幅しか見ないため、サイドバーのカードにも「広い画面用」の2段組みが当たってしまいます。判断の基準を親コンテナに移すのがコンテナクエリです。
| 切り替えの基準 | 使う仕組み | 向いている対象 |
|---|---|---|
| ビューポート幅 | @media | ページ全体の骨格。ヘッダー、段組みの数、サイドバーの有無 |
| 親コンテナ幅 | @container | 置き場所が複数あるコンポーネント。カード、フォーム、記事リスト |
| OSのユーザー設定 | @media (prefers-*) | アニメーションの量、ライト/ダークの配色 |
/* 1) 親をコンテナとして登録する */
.card-area {
container-type: inline-size;
}
/* 2) ベース = 縦積み */
.card {
display: grid;
gap: 16px;
}
/* 3) 親コンテナが480px以上のときだけ横並びにする */
@container (width >= 480px) {
.card {
grid-template-columns: 160px 1fr;
}
}この書き方なら、同じ .card を本文カラムに置けば横並び、サイドバーに置けば縦積みになります。コンポーネント側がCSSを持ち歩けるので、置き場所ごとの上書きが要りません。container-type に指定できる値、コンテナ名(container-name)による絞り込み、コンテナ基準の単位 cqw / cqi の使い方はCSSコンテナクエリの使い方で解説しています。
モバイルの100vh問題は dvh / svh / lvh で解く
スマートフォンで height: 100vh を指定したのに、画面下部がアドレスバーに隠れて収まらない——これは vh の定義に理由があります。MDNは、現時点ですべての既定のビューポート単位(vh や vw)は対応する大ビューポート単位(lvh、lvw)と同じであると記載しています。つまり 100vh は「ブラウザUIが収納された状態の高さ」であり、UIが展開されている間はその分だけコンテンツが隠れます(MDN: <length>)。
この差を扱うために、小・大・動的の3種類のビューポートサイズに対応した単位が用意されています。Baseline: widely available には2025年6月5日に到達しています(webstatus.dev: Viewport unit variants)。
| 単位 | 基準にする高さ | 使いどころ |
|---|---|---|
| 100vh | 大ビューポート(lvhと同じ) | ブラウザUIが展開中は下部が隠れる。全画面の指定には向かない |
| 100svh | 小ビューポート。UIが展開された状態の高さ | ファーストビューを必ず画面内に収めたいとき |
| 100lvh | 大ビューポート。UIが収納された状態の高さ | 背景など、多少隠れても支障がない要素 |
| 100dvh | 動的。UIの伸縮に追随して値が変わる | 常にぴったり合わせたいとき。ただし値が安定しない |
.hero {
/* 小ビューポート基準。ブラウザUIが出ていても収まる */
min-height: 100svh;
}迷ったら svh を選びます。dvh はUIの伸縮に追随するぶん見た目は理想的ですが、MDNは動的ビューポート単位についてスクロール中にコンテンツのサイズが変わり、UIの劣化やパフォーマンスへの影響につながる可能性があると注意を記載しています。全画面のヒーローには svh、値の変化が問題にならない装飾には dvh、と分けるのが安全です。
レスポンシブが崩れていないかを数値で判定する
「開発者ツールで見た目を確認する」だけでは、数pxのはみ出しを見落とします。レスポンシブが崩れているかどうかは、documentElement の scrollWidth と clientWidth を比べれば数値で判定できます。ブラウザのコンソールに次の2行を貼るだけです。
合否ラインは overflow === 0
const d = document.documentElement;
console.log({ scrollWidth: d.scrollWidth, clientWidth: d.clientWidth, overflow: d.scrollWidth - d.clientWidth });| 結果 | 意味 | 次にやること |
|---|---|---|
| overflow === 0 | 合格。横方向にはみ出している要素はない | 次の画面幅で測る |
| overflow > 0 | 不合格。その差分ぶん、コンテンツが画面の外へ出ている | 下のスクリプトではみ出し要素を名指しする |
開発者ツールのデバイスツールバーで幅を切り替えながら、375px / 390px / 768px / 1280px の4点すべてで overflow === 0 になれば合格とします。375pxと390pxは主要なスマートフォンの論理幅、768pxと1280pxはタブレットとデスクトップの代表値です。デバイスツールバーの出し方はChrome DevToolsでの検証手順にまとめています。
不合格ならはみ出している要素を名指しする
overflow が0より大きかったら、犯人を目視で探さずスクリプトに出させます。要素の右端がビューポートの右端を超えているものだけを列挙します。
[...document.querySelectorAll('*')]
.filter(el => el.getBoundingClientRect().right > document.documentElement.clientWidth)
.forEach(el => console.log(el, el.getBoundingClientRect().right));コンソールに出た要素にマウスを乗せると、ページ上の該当箇所がハイライトされます。最も多い原因は、この記事の前半で扱った width: 100% と padding の併用です。box-sizing が content-box のままだと、左右20pxのpaddingで375pxの画面が415pxになります。
*, *::before, *::after {
box-sizing: border-box;
}これを当ててから最初のスクリプトをもう一度実行し、scrollWidth と clientWidth が同じ値になれば解消です。まだ差が残る場合は、2つ目のスクリプトが名指しした要素を1つずつ確認する、という同じ手順を繰り返します。
ここで body { overflow-x: hidden; } を当てて隠すのは避けてください。はみ出し自体は残ったまま横スクロールバーだけが消えるので、原因が見えなくなります。加えて、祖先要素に overflow: hidden が付くとその要素がスクロールコンテナになり、子孫の position: sticky が意図した位置で止まらなくなります(MDN: position)。直すのは症状ではなく原因側です。
よくある質問
Q. ブレークポイントはいくつ必要ですか?
多くのサイトは768pxと1024pxの2本で足ります。端末名から決めるのではなく、自分のコンテンツを縮めていって最初に読みづらくなった幅を境界にし、必要になった時点で足していくのが効率的です。
Q. min-width と max-width はどちらを基準にすべきですか?
min-width(または width >=)に統一します。ベースを狭い画面向けにし、広い画面だけを上書きする一方向の設計にすると、どの宣言が勝つかを追う必要がなくなります。両方を混ぜると768pxと769pxのような境界の重複が生まれます。
Q. フォントサイズはpxとremのどちらで書くべきですか?
本文はremを基本にします。ユーザーがブラウザの文字サイズ設定を変えたときに追随するためです。流体化したい場合は clamp(1rem, 0.95rem + 0.5vw, 2rem) のように最小値をremで、最大値も相対単位かつ最小値の2倍以上にします。
Q. スマホで横スクロールが出ます
この記事の検証パートにあるスクリプトではみ出し要素を特定し、原因側を直します。最も多いのは width: 100% と padding の併用で、box-sizing: border-box の適用で解消します。overflow-x: hidden で隠すのは、原因が残るうえに position: sticky を壊す副作用があるため避けます。
Q. 画像の縦横比が崩れます
img { max-width: 100%; height: auto; } を基本にし、HTML側の width と height 属性も必ず書きます。カードのサムネイルのように枠を固定したい箇所は aspect-ratio と object-fit: cover を組み合わせると安定します。
Q. メディアクエリとコンテナクエリはどう使い分けますか?
ページ全体の骨格はビューポート幅を見る @media、置き場所が複数あるコンポーネントは親コンテナ幅を見る @container です。サイドバーと本文カラムの両方に置くカードのように、同じ部品が違う幅の器に入る場合はコンテナクエリが適しています。
Q. テーブルはどうレスポンシブ化しますか?
最小の対応は親要素に overflow-x: auto を付けて横スクロールさせる方法です。列同士の比較が読みづらい場合は、狭い画面のときだけ display: grid で行をカード状に組み替えると、1行ぶんの情報が縦に並んで読めるようになります。
まとめ
レスポンシブデザインは、viewportメタタグ・min-widthの単方向・box-sizing: border-box の3点を押さえれば、崩れる原因のほとんどを先回りで潰せます。そのうえで、切り替えの基準をコンポーネントに移したいときは @container、モバイルの全画面表示には svh という道具が使えます。
最後は必ず数値で確認してください。375px / 390px / 768px / 1280px の4点で scrollWidth - clientWidth が0なら、そのページに横方向のはみ出しはありません。書いたら測る、という流れができれば、レスポンシブの手戻りは大きく減ります。次の一手としては、CSSテクニック集から自分のサイトで使えそうな表現を1つ選び、同じ4点で測りながら足していくのがおすすめです。
