Old-School Front-End / 2026 ・ No.05

いにしえのフロントエンド入門

第5回 CSS ② ── レイアウトとスマホ:flex と grid で並べ、画面の幅に合わせる

公開

同じ申請一覧を、パソコン、タブレット、スマホの3つの画面幅で並べた写真。パソコンではヘッダーが横一列で、まとめの箱が3つ横に並ぶ。スマホではまとめの箱が縦に並び、見出しと新規申請ボタンは横に並んだまま
今回のゴール。1枚の HTML と CSS で、パソコン・タブレット・スマホに合わせて並び方が変わります(筆者のデモページを、クラウド上の Chromium で撮影。スマホは Playwright のスマホ表示(390×844)。2026年10月17日。端末やブラウザの枠は説明のために描き足したもの)

AI に頼まず、一行ずつ手で書く「いにしえの」フロントエンド入門。第5回はレイアウトです。
前回服を着せた申請一覧を、スマホで開くと……残念なことになっていました。
今回は flex(一列に並べる)と grid(升目に並べる)、そしてメディアクエリ(画面の幅で切り替える)を使って、
1枚の HTML と CSS で、パソコンでもスマホでも読める画面にします。

一

今回のゴール

新井先輩……。課長にスマホで見せたら、「状態の欄、お経みたいだね」って言われました。

古井だから、スマホで開く前に第5回を読めと言ったんだ。

新井AI に「スマホ対応して」と頼んだら、CSS が倍の長さになって返ってきて、もうどこを直せばいいのか……。

古井並べ方の道具は、実は3つしかない。それさえ分かれば、スマホ対応は怖くないぞ。

いまここ:第5回

1 準備 2 HTML の骨組み 3 フォーム 4 CSS の基本 5 レイアウト 6 Bootstrap 7 JS の基本 8 DOM・イベント 9 入力チェック 10 通信 11 ログイン 12 承認の流れ 13 集計と印刷 14 jQuery 15 仕上げ いまここ 準備 HTML CSS JavaScript 実戦 仕上げ

まずは現状確認です。申請一覧をスマホ(幅 390px)で開いてみました。

スマホの画面を3台並べた写真。1 meta viewport なしでは、ページ全体が小さく縮んで文字が読めない。2 第4回の CSS のままでは、表が押しつぶされて件名が細く折り返し、状態のバッジが縦書きになる。3 第5回では、まとめの箱が縦に並び、表は横にスクロールできる
①meta viewport を消した場合。②第4回の CSS のまま。③今回の CSS。②の状態のバッジは、まるで縦書きのお札です(筆者のデモページを、クラウド上の Chromium で撮影。スマホは Playwright のスマホ表示(390×844)。2026年10月17日。端末やブラウザの枠は説明のために描き足したもの)

②が前回の 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 なし width=device-width 980px × 0.398 390px × 1 16px の文字が約 6.4px に 16px の文字は 16px のまま

meta なしでは 0.398 倍に縮められ、16px の文字が約 6.4px になります。指で拡大しないと読めません。width=device-width は「端末の画面の幅で組んで」、initial-scale=1 は「最初は等倍で」という意味です。スマホ対応の第一歩は、この1行です。

ただし、この1行があっても②のように崩れます。meta viewport は「スマホの幅で組む」ところまで。その幅に収まるように並べるのは、CSS の仕事です。

三

箱の並べ方は3つ

第4回で「すべての要素は箱」だと学びました。その箱の並べ方は、大きく分けて3つです。

箱の並べ方3種類

通常の流れ 縦に積む(初期の並べ方) 例:本文の段落 flex 一方向に並べる 例:ヘッダー・見出し行 grid 縦横の升目に入れる 例:まとめの箱 block block a block 主軸(並ぶ向き) 列と行を先に決める
  • 通常の流れ ── 何も指定しないときの並び方。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; } .site nav .user gap margin-left: auto 主軸(横) justify-content 交差軸(縦) align-items: center 親に display: flex を書くと、子が主軸の向きに一列に並ぶ margin-left: auto は「左の余りを全部ちょうだい」。右端に寄せる定番の書き方
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 と子の位置(実測)

justify-content: 子の左端の位置(実測 px、すき間 8px) flex-start 0 88 176 flex-end 224 312 400 center 112 200 288 space-between 0 200 400 最後の子に margin-left: auto 0 88 400 space-between は両端に寄せて、残りを等分。auto は一つだけ右端へ

ヘッダーで使った 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 の列の数と幅(実測)

repeat(auto-fit, minmax(200px, 1fr)) 画面 390px(中身 358px)→ 1列 358 画面 600px(中身 568px)→ 2列 278 278 画面 768px(中身 720px)→ 3列 232 232 232 画面 1024px(中身 912px)→ 3列 296 296 296 0px 同じ 1024px で auto-fill にすると → 4列目が空のまま残る 219 219 219 219 入るだけ列を作り(最小 200px)、余りは 1fr で均等に分ける auto-fit は空の列を 0 に畳む。auto-fill は残す

スマホでは1列、600px なら2列、768px なら3列。メディアクエリを1行も書かずに、列の数が勝手に変わります。1024px では4列分の場所がありますが、箱は3つしかないので、auto-fit は空の4列目を 0px に畳み、3つの箱が横いっぱいに広がります。auto-fill にすると空の列が残り、右側に1列分の空きができます。今回は「箱が横いっぱいに広がる」auto-fit を選びました。

POINT

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行ずつ) */
}
スマホの表の部分を2枚並べた写真。1 スクロール前は No.、申請日、件名が見える。2 表を横にスワイプすると、金額と状態の列が現れる。ページ全体は動かない
①スクロール前。②表だけを横にスワイプしたところ。ページは 390px のまま、表だけが動きます(筆者のデモページを、クラウド上の Chromium で撮影。スマホは Playwright のスマホ表示(390×844)。2026年10月17日。端末やブラウザの枠は説明のために描き足したもの)
  • 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)

画面の幅と、切り替わる場所(実測)

320 444 656 768 1024 1280 画面の幅(px) 共通のスタイル(スマホ向けに先に書く) @media (min-width: 768px) ここから足す まとめ 1列 まとめ 2列 まとめ 3列 表は横スクロール 672 青と点線は、メディアクエリを書かなくても自然に変わる部分

面白いのは、メディアクエリで切り替えているのはヘッダーまわりと 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;
  }
}
パソコンの幅の申請一覧に赤枠と番号を付けたもの。1 ヘッダー(flex)、2 利用者名(margin-left: auto)、3 見出しとボタンの行(flex)、4 まとめの箱(grid)、5 表の囲み(横スクロール)
今回レイアウトを付けた場所。①body > header、②.user、③.page-head、④.summary、⑤.table-wrap(筆者のデモページを、クラウド上の Chromium で撮影。スマホは Playwright のスマホ表示(390×844)。2026年10月17日。端末やブラウザの枠は説明のために描き足したもの)
  • 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 で確かめる ── メディアクエリの中の指定が、その幅で効いているか(取り消し線が付いていないか)を見る
NOTE

デバイスモードは便利な近似で、本物のスマホとまったく同じではありません。書体や、指で触ったときの操作感は実機で確かめましょう。AI に「スマホ対応して」と頼んだときも、320px から 1280px まで幅を動かして、崩れる場所がないか自分の目で見るのが、いにしえの検品です。

結 — 一枚まとめと次回予告

  1. スマホ対応の第一歩は meta viewport。ないと、980px の画面として組まれて縮小される。
  2. 箱の並べ方は 通常の流れ・flex・grid の3つ。一列なら flex、升目なら grid。
  3. flex は 主軸と交差軸。justify-content と align-items で並べ、margin-left: auto で右端へ。
  4. grid の repeat(auto-fit, minmax(200px, 1fr)) は、メディアクエリなしで列の数が変わる。表は囲みの中だけ横スクロール。
  5. メディアクエリはスマホ向けを先に書き、min-width で足す。ブレークポイントは端末ではなく中身で決める。

新井自分で書いたら、CSS が AI 版の半分になりました。でも、ボタンとか表とか、毎回ここまで手で書くんですか……?

古井いい質問だ。次回は Bootstrap。いにしえの人々が「毎回書くのはもう嫌だ」と作った道具箱を開けるぞ。中身が分かったいまなら、道具に使われずに済む。

参考

ページの写真は、筆者が作ったデモページを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

← 記事一覧へ