Old-School Front-End / 2026 ・ No.05
いにしえのフロントエンド入門
第5回 CSS ② ── レイアウトとスマホ:flex と grid で並べ、画面の幅に合わせる
公開
AI に頼まず、一行ずつ手で書く「いにしえの」フロントエンド入門。第5回はレイアウトです。
前回服を着せた申請一覧を、スマホで開くと……残念なことになっていました。
今回は flex(一列に並べる)と grid(升目に並べる)、そしてメディアクエリ(画面の幅で切り替える)を使って、
1枚の HTML と CSS で、パソコンでもスマホでも読める画面にします。
今回のゴール
新井先輩……。課長にスマホで見せたら、「状態の欄、お経みたいだね」って言われました。
古井だから、スマホで開く前に第5回を読めと言ったんだ。
新井AI に「スマホ対応して」と頼んだら、CSS が倍の長さになって返ってきて、もうどこを直せばいいのか……。
古井並べ方の道具は、実は3つしかない。それさえ分かれば、スマホ対応は怖くないぞ。
いまここ:第5回
まずは現状確認です。申請一覧をスマホ(幅 390px)で開いてみました。
②が前回の CSS のままの状態です。表がスマホの幅に押し込まれて、件名は数文字ずつ折り返し、状態のバッジは1文字ずつ縦に並んでしまいました。①は、さらに HTML の meta viewport を消したもの。③が今回のゴールです。HTML の変更は囲みの div を2つ足すだけで、あとは CSS で解決します。
スマホの画面を知る:meta viewport
第2回から head に書いてきた、この1行を覚えていますか。
<meta name="viewport" content="width=device-width, initial-scale=1">
これがないと、スマホのブラウザは「このページはパソコン用だろう」と考え、幅 980px の画面があるつもりでページを組み、それを縮めて表示します。スマホの画面で開いて測った値です。
# スマホ(390×844)で開いて測った値 meta なし 第4回のまま 第5回 ページを組む幅 980px 390px 390px 表示の倍率 0.398 1 1 表の幅 912px 342px 640px(枠は 358px、横スクロール) 件名の列の幅 358px 73px 251px 状態のバッジの高さ 27px 68px 27px
meta viewport の有無
meta なしでは 0.398 倍に縮められ、16px の文字が約 6.4px になります。指で拡大しないと読めません。width=device-width は「端末の画面の幅で組んで」、initial-scale=1 は「最初は等倍で」という意味です。スマホ対応の第一歩は、この1行です。
ただし、この1行があっても②のように崩れます。meta viewport は「スマホの幅で組む」ところまで。その幅に収まるように並べるのは、CSS の仕事です。
箱の並べ方は3つ
第4回で「すべての要素は箱」だと学びました。その箱の並べ方は、大きく分けて3つです。
箱の並べ方3種類
- 通常の流れ ── 何も指定しないときの並び方。block(p、h2、section など)は縦に積み、inline(a、span など)は文の中に流れる。第4回までの申請一覧は、ほぼこれだけでできていた
- flex ── 親に display: flex を書くと、子が一方向に一列に並ぶ。ヘッダーの中身や、見出しとボタンのような「横一列」が得意
- grid ── 親に display: grid を書き、列と行の升目を先に決めて子を入れる。カードや箱を何列にも並べるのが得意
古井flex と grid がなかった頃は、float という「文章の横に画像を回り込ませる」ための技で、無理やり段組みをしていたもんだ。
新井回り込み用の技で、画面全体を?
古井そう。だから段の高さはそろわないし、後始末のおまじない(clearfix)も要った。いまは素直に flex と grid を使えばいい。いにしえの苦労は、いにしえに置いていこう。
flex:一列に並べる
パソコンの幅では、縦に3段積みだったヘッダーを、横一列にしたい。これが flex の出番です。
ヘッダーを flex にする
body > header {
display: flex; /* 子を一列に並べる */
flex-wrap: wrap; /* 入りきらなければ折り返す */
align-items: center; /* 縦の真ん中でそろえる */
gap: 8px 32px; /* 縦のすき間 8px、横のすき間 32px */
}
.user {
margin: 0 0 0 auto; /* 左の余りを全部もらって、右端へ */
}
flex には二つの向きがあります。子が並ぶ向きが主軸(初期は横)、それと直角が交差軸(縦)。主軸の並べ方は justify-content、交差軸のそろえ方は align-items で決めます。justify-content の値で子の位置がどう変わるのか、実験ページで測ってみました。
justify-content と子の位置(実測)
ヘッダーで使った margin-left: auto は、flex の定番の小技です。flex の子に auto の margin を付けると、余った空きを全部その margin が受け取るので、その子だけが右端に押しやられます。「ロゴとナビは左、利用者名だけ右」という配置が、1行で書けます。
見出しとボタンの行も flex です。HTML に div class="page-head" の囲みを足し、justify-content: space-between で両端に分けました。こちらはスマホでも横並びのまま。h1 とボタンは短いので、狭い画面でも一列に入るからです(入らなければ flex-wrap で折り返します)。
grid:升目に並べる
まとめの箱は、前回は inline-block と width: 220px で並べていました。幅を決め打ちしているので、パソコンでは右側が余り、スマホでは1つずつ縦に並んで右側がぽっかり空きます。これを grid に置き換えます。
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
2行目が呪文のようですが、内側から読めば簡単です。
- minmax(200px, 1fr) ── 1列の幅は「最小 200px、最大 1fr」。fr は「残りの幅の取り分」という grid 専用の単位で、1fr ずつなら均等に分ける
- repeat(auto-fit, …) ── その列を「入るだけ」繰り返す。200px の列が何本入るかは、ブラウザが幅から計算する
画面の幅を変えて、実際の列の幅を測りました。
auto-fit の列の数と幅(実測)
スマホでは1列、600px なら2列、768px なら3列。メディアクエリを1行も書かずに、列の数が勝手に変わります。1024px では4列分の場所がありますが、箱は3つしかないので、auto-fit は空の4列目を 0px に畳み、3つの箱が横いっぱいに広がります。auto-fill にすると空の列が残り、右側に1列分の空きができます。今回は「箱が横いっぱいに広がる」auto-fit を選びました。
grid が覚えるのは「最小の幅」だけで、何列にするかはブラウザ任せです。「スマホは1列、タブレットは2列、パソコンは3列」と画面の種類ごとに書き分けるより、ずっと短く、想定外の幅にも強い CSS になります。
表は、表だけをスクロール
表は、行と列がそろっていることに意味があります。無理に縮めると②のように読めなくなり、かといってページ全体を横にはみ出させると、ページごと左右にぶれてしまいます。そこで、表を div で囲み、その囲みの中だけを横にスクロールさせます。
<div class="table-wrap" tabindex="0" role="region" aria-label="申請の一覧"> <table> … </table> </div>
.table-wrap {
overflow-x: auto; /* はみ出したら、この箱の中で横スクロール */
}
table {
width: 100%;
min-width: 640px; /* これ以上は縮めない */
}
.amount, td time, .status {
white-space: nowrap; /* 金額・日付・状態は折り返さない(実際は各ルールに1行ずつ) */
}
- min-width: 640px ── 表の最小の幅を決める。これより狭い画面では、表は縮まずに囲みの中ではみ出し、そこで横スクロールになる
- white-space: nowrap ── 日付や金額、バッジが途中で折り返さないようにする。「2026-10-」と「16」が泣き別れになるのを防ぐ
- tabindex="0" ── 囲みにキーボードのフォーカスが入るようにする。フォーカスした状態で矢印キーを押せば、マウスがなくても横にスクロールできる
- role="region" と aria-label ── フォーカスできる場所には名前が要る。読み上げソフトに「申請の一覧」という区画だと伝える
メディアクエリ:幅で切り替える
ヘッダーを横一列にするのは、パソコンのように広い画面のときだけにしたい。スマホで横一列にすると、ロゴとナビと利用者名がぎゅうぎゅうになるからです。こういう「画面の条件で CSS を切り替える」のがメディアクエリです。
/* ここまでは、すべての画面に効く(スマホ向けに書く) */
body > header { background: var(--brand); padding: 12px 24px; }
@media (min-width: 768px) {
/* 画面の幅が 768px 以上のときだけ、追加で効く */
body > header { display: flex; … }
}
今回はスマホ向けを先に書き、広い画面のための指定を min-width で足していく書き方(モバイルファースト)にしました。狭い画面ほど指定が少なく、シンプルに保てます。画面の幅を 1px ずつ変えて、見た目が切り替わる場所を調べました。
# 画面の幅を 300px から 1300px まで 1px ずつ変え、変わったところ 300px〜 まとめ 1列・表は横スクロール・ヘッダーは縦積み 444px〜 まとめ 2列 656px〜 まとめ 3列 672px〜 表が枠に収まる(横スクロールなし) 768px〜 メディアクエリが効く → ヘッダーが横一列(display: flex)
画面の幅と、切り替わる場所(実測)
面白いのは、メディアクエリで切り替えているのはヘッダーまわりと main の余白だけ、ということです。まとめの列の数(grid)と表のスクロール(overflow)は、中身の幅に合わせて自然に変わります。メディアクエリは最後の手段くらいに考えて、まずは「幅に合わせて自然に伸び縮みする」書き方を選ぶのがこつです。
新井768px って、何か決まりがあるんですか? iPad の幅とか?
古井端末の名前で決めると、新しい機種が出るたびに追いかけっこになる。決めるのは中身だ。ヘッダーのロゴとナビと名前が一列に気持ちよく収まる幅、それが今回はだいたい 768px だった、というだけさ。
完成した CSS
HTML の変更点です(太字が足した行)。
<main>
<div class="page-head">
<h1>申請一覧</h1>
<p><a class="button" href="form.html">+ 新規申請</a></p>
</div>
<section>
<h2>今月のまとめ</h2>
<ul class="summary">
<li>今月の申請:4 件</li>
<li>承認待ちの金額:<strong>32,400 円</strong></li>
<li>差し戻し:1 件</li>
</ul>
</section>
<section>
<h2>申請の一覧</h2>
<div class="table-wrap" tabindex="0" role="region" aria-label="申請の一覧">
<table>
<caption>2026年10月の申請(新しい順)</caption>
<thead>
…
</table>
</div>
</section>
そして、css/style.css の全体です。太字が第4回から変えた・足した行です。
/* ===== 1. 全体の約束(色の変数・箱の大きさの数え方・文字) ===== */
:root {
--ink: #1f2b2a;
--muted: #5b6b75;
--brand: #1f4e6b;
--accent: #c0392b;
--line: #d7dee5;
--bg: #f4f6f9;
}
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, "Hiragino Sans", "Noto Sans JP", "Yu Gothic UI", sans-serif;
line-height: 1.6;
color: var(--ink);
background: var(--bg);
}
/* ===== 2. ヘッダー ===== */
body > header {
background: var(--brand);
color: #fff;
padding: 12px 24px;
}
body > header a {
color: #fff;
text-decoration: none;
}
.site {
margin: 0;
font-size: 1.125rem;
font-weight: 700;
}
.site img {
vertical-align: middle;
}
nav ul {
list-style: none;
margin: 8px 0 0;
padding: 0;
}
nav li {
display: inline-block;
margin-right: 16px;
}
nav a:hover,
nav a:focus-visible {
text-decoration: underline;
}
.user {
margin: 4px 0 0;
font-size: 0.875rem;
opacity: 0.85;
}
/* ===== 3. 本体と見出し ===== */
main {
max-width: 960px;
margin: 0 auto;
padding: 16px;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
/* 見出しとボタンを左右に */
.page-head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.page-head p {
margin: 0;
}
h2 {
margin: 32px 0 12px;
padding-left: 10px;
border-left: 4px solid var(--accent);
font-size: 1.125rem;
}
/* ボタンのように見せるリンク */
.button {
display: inline-block;
padding: 8px 16px;
border-radius: 6px;
background: var(--accent);
color: #fff;
font-weight: 700;
text-decoration: none;
}
.button:hover {
background: #a93226;
}
.button:focus-visible {
outline: 3px solid #f5b041;
outline-offset: 2px;
}
/* まとめ:グリッドで、入るだけ横に並べる(最小 200px) */
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
list-style: none;
margin: 0;
padding: 0;
}
.summary li {
padding: 12px 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
}
.summary strong {
display: block;
color: var(--accent);
font-size: 1.25rem;
}
/* ===== 4. 表 ===== */
/* 表:狭い画面では、表だけを横にスクロールさせる */
.table-wrap {
overflow-x: auto;
}
.table-wrap:focus-visible {
outline: 3px solid #f5b041;
}
table {
width: 100%;
min-width: 640px;
border-collapse: collapse;
border: 1px solid var(--line);
background: #fff;
}
caption {
padding-bottom: 8px;
color: var(--muted);
font-size: 0.875rem;
text-align: left;
}
th,
td {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
text-align: left;
}
thead th {
background: #eef2f6;
color: var(--muted);
font-size: 0.875rem;
}
tbody tr:nth-child(even) {
background: #fafbfc;
}
tbody tr:hover {
background: #fdf2e9;
}
.amount {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
td time {
white-space: nowrap;
}
/* 状態のバッジ(色だけに頼らず、文字も残す) */
.status {
display: inline-block;
padding: 2px 10px;
border: 1px solid;
border-radius: 999px;
font-size: 0.8125rem;
font-weight: 700;
white-space: nowrap;
}
.status-submitted { color: #8a5a00; background: #fff4d6; }
.status-approved { color: #1d6b3a; background: #e3f4e9; }
.status-returned { color: #a93226; background: #fdecea; }
.status-paid { color: #4d5b66; background: #eceff2; }
/* ===== 5. フッター ===== */
body > footer {
padding: 24px;
color: var(--muted);
text-align: center;
}
/* ===== 6. 768px 以上の画面(タブレット・パソコン) ===== */
@media (min-width: 768px) {
/* ヘッダーを横一列に */
body > header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 32px;
}
nav ul {
margin: 0;
}
/* 利用者名を右端へ押しやる */
.user {
margin: 0 0 0 auto;
}
main {
padding: 24px;
}
}
- main の padding ── スマホでは 16px、768px 以上で 24px。狭い画面では、余白も少し詰める
- .summary li から width と inline-block を削除 ── 箱の幅は grid の列が決めるので、子に幅を書かない。書くと grid と喧嘩する
- h1 の margin を 0 に ── 見出しとボタンの縦の位置は、.page-head の align-items: center にそろえてもらう
新井AI の「スマホ対応版」は、メディアクエリが5つもあって、同じ指定を何回も上書きしていました。
古井今回のメディアクエリは1つだけ。伸び縮みは grid と overflow に任せたからな。短い CSS は、直すときに迷わない。
確かめ方
スマホの表示を、いちいち実機に送らなくても確かめる方法があります。Chrome の開発者ツールのデバイスモードです。
- 開き方 ── 開発者ツールを開き、左上のスマホとタブレットの形のアイコン(デバイスツールバー)を押す。ショートカットは Windows なら Ctrl+Shift+M、Mac なら Cmd+Shift+M
- 幅を動かす ── 画面の端をドラッグして幅を変え、どこで崩れるかを探す。今回のように 320px くらいの狭い幅も試す
- Styles で確かめる ── メディアクエリの中の指定が、その幅で効いているか(取り消し線が付いていないか)を見る
デバイスモードは便利な近似で、本物のスマホとまったく同じではありません。書体や、指で触ったときの操作感は実機で確かめましょう。AI に「スマホ対応して」と頼んだときも、320px から 1280px まで幅を動かして、崩れる場所がないか自分の目で見るのが、いにしえの検品です。
結 — 一枚まとめと次回予告
- スマホ対応の第一歩は meta viewport。ないと、980px の画面として組まれて縮小される。
- 箱の並べ方は 通常の流れ・flex・grid の3つ。一列なら flex、升目なら grid。
- flex は 主軸と交差軸。justify-content と align-items で並べ、margin-left: auto で右端へ。
- grid の repeat(auto-fit, minmax(200px, 1fr)) は、メディアクエリなしで列の数が変わる。表は囲みの中だけ横スクロール。
- メディアクエリはスマホ向けを先に書き、min-width で足す。ブレークポイントは端末ではなく中身で決める。
新井自分で書いたら、CSS が AI 版の半分になりました。でも、ボタンとか表とか、毎回ここまで手で書くんですか……?
古井いい質問だ。次回は Bootstrap。いにしえの人々が「毎回書くのはもう嫌だ」と作った道具箱を開けるぞ。中身が分かったいまなら、道具に使われずに済む。
参考
- MDN — <meta name="viewport">
- MDN — Viewport concepts
- MDN — Basic concepts of flexbox
- MDN — Basic concepts of grid layout
- MDN — Relationship of flexbox to other layout methods
- MDN — Using media queries
- MDN — Responsive web design
- Chrome for Developers — Simulate mobile devices with device mode
ページの写真は、筆者が作ったデモページを2026年10月17日にクラウド上の Chromium(Playwright)で撮影したものです。スマホとタブレットは Playwright のスマホ表示(isMobile)による再現で、実機の画面ではありません。撮影環境には Hiragino Sans がないため、日本語は Noto Sans CJK JP で表示されています。表示の幅や倍率、表と列の幅、バッジの高さ、flex の子の位置、grid の列の幅、切り替わる画面の幅は、同じ Chromium で測った実際の値です(文字の幅によって少し変わります)。index.html と style.css は Nu Html Checker(vnu)でエラーがないことを確かめています。登場人物は架空の人物です。
いにしえのフロントエンド入門 第5回 — CSS ②:レイアウトとスマホ / 2026.10