Old-School Front-End / 2026 ・ No.06
いにしえのフロントエンド入門
第6回 CSS ③ ── Bootstrap:いにしえの道具箱を、中身を知ったうえで使う
公開
AI に頼まず、一行ずつ手で書く「いにしえの」フロントエンド入門。第6回は Bootstrap です。
第4・5回で手書きしたものを、クラスを付けるだけで手に入れられる道具箱。
今回は申請一覧と申請フォームを Bootstrap で書き直し、どのクラスが、手で書いたどの CSS にあたるのかを確かめます。
便利さの裏にある重さと、AI がよく踏む落とし穴も、実際に測ってみました。
今回のゴール
新井先輩、Bootstrap って AI がよく使ってくるやつですよね。クラスをいっぱい書くだけで、それっぽくなる。
古井そう、「毎回同じ CSS を書くのはもう嫌だ」という人たちが作った道具箱だ。2011年に Twitter 社の開発者が公開して、いまや定番になった。
新井じゃあ、最初から Bootstrap を教えてくれればよかったのに……。
古井中身を知らずに使うと、道具に使われる。第4・5回で手書きしたいまなら、クラスの名前を見ただけで「ああ、あの CSS だ」と分かるはずだ。
いまここ:第6回
今回使うのは、執筆時点の安定版 Bootstrap 5.3.8(2025年8月26日公開)です。2026年10月8日には次の大きな版 6.0.0 の最初の試作版(alpha.1)が出ましたが、まだ試作段階なので、業務システムには 5.3 系を使います。まずは、第5回の手書きの CSS 版と並べてみましょう。
見た目はほぼ同じです。違いは中身。①は自前の CSS が205行(空行を除く。以下同じ)、②は Bootstrap のクラスを HTML に付けて、自前の CSS(custom.css)は31行だけ。CSS を書く代わりに、クラスを選ぶ。これが Bootstrap の使い方です。
Bootstrap を読み込む
Bootstrap の正体は、名前の付いた CSS のルールの詰め合わせ(bootstrap.min.css)と、ナビの開け閉めなどの動きを担う JavaScript(bootstrap.bundle.min.js)です。
読み込む順番
ファイルの置き方は二通りあります。今回は npm で取ってきたファイルをフォルダに置きました(vendor/bootstrap/)。社内のネットワークだけで動かす業務システムでは、こちらが安心です。
<!-- 自分のフォルダに置く(今回) --> <link rel="stylesheet" href="vendor/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="css/custom.css"> … <script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script> <!-- CDN(配信サービス)から読み込む場合 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
CDN を使うときの integrity は、「届いたファイルが、この指紋(ハッシュ)と一致しなければ使わない」という指定です。配信元のファイルが書き換えられても、気づかずに読み込むことを防げます。上の値は、npm の 5.3.8 のファイルから筆者が計算し、jsDelivr から取ってきたファイルとも一致することを確かめたものです。
クラスの読み方
Bootstrap のクラスには、2種類あります。navbar や card のような部品(コンポーネント)と、mb-3 や d-flex のような、1つのことだけをする小道具(ユーティリティ)です。小道具の名前は、規則さえ分かれば読めます。
ユーティリティの名前を分解する
第4・5回で手書きした CSS と、Bootstrap のクラスを対応させると、こうなります。
| 手書きの CSS(第4・5回) | Bootstrap のクラス |
|---|---|
| body > header { display: flex; … } | navbar navbar-expand-md + container |
| .user { margin-left: auto; } | ms-md-auto |
| .page-head { display: flex; justify-content: space-between; … } | d-flex flex-wrap justify-content-between align-items-center gap-3 |
| .summary { display: grid; … } | row g-3 + col-12 col-sm-6 col-lg-4 |
| .summary li { border: 1px solid; border-radius: 8px; … } | card + card-body |
| .table-wrap { overflow-x: auto; } | table-responsive |
| tbody tr:nth-child(even)、tr:hover | table-striped、table-hover |
| .amount { text-align: right; white-space: nowrap; } | text-end text-nowrap |
| .status-approved { color: …; background: …; } | badge rounded-pill text-bg-success |
| @media (min-width: 768px) { … } | クラス名の中の -md- |
新井本当だ……。d-flex は display: flex、ms-md-auto は 768px 以上で margin-left: auto。もう呪文に見えません!
古井それが手書きをしてきたご褒美だ。
グリッドと画面の幅
第5回では grid の auto-fit で「入るだけ並べる」を書きました。Bootstrap のグリッドは考え方が違い、横を12列に分けて、何列分を使うかをクラスで指定します。
12列のグリッド
col-12 col-sm-6 col-lg-4 は、「ふだんは12列(1つずつ)、sm 以上で6列分(2つ並ぶ)、lg 以上で4列分(3つ並ぶ)」と読みます。sm や lg は、Bootstrap が決めている画面の幅の区切り(ブレークポイント)です。bootstrap.css の中の min-width の値を数えると、576・768・992・1200・1400px の5か所でした。実際に幅を変えて測った見た目です。
画面の幅の区切りと、実測した見た目
container は、中身の幅を区切りごとに決まった値にそろえる箱です。第5回で書いた max-width: 960px の代わりになります。ナビの navbar-expand-md は「md(768px)以上で横に展開、それより狭ければ三本線のボタンにたたむ」という意味です。
部品を使う:ナビ・カード・表・バッジ・ボタン
部品(コンポーネント)は、決まった組み合わせで HTML を書くと、見た目と動きがそろって手に入ります。スマホでは、ナビが三本線のボタン(通称ハンバーガー)にまとまります。
開け閉めの動きは CSS だけではできないので、body の最後で読み込んだ bootstrap.bundle.min.js が担当しています。ボタンの data-bs-toggle="collapse" と data-bs-target="#mainNav" が、「押したら #mainNav を開け閉めして」という合図です。JavaScript の中身は第7回から自分で書いていきます。
- card ── 枠線と角の丸みと余白が付いた箱。中身は card-body に入れる。h-100 で、並んだカードの高さをそろえた
- table ── table を付けるだけで罫線と余白が付く。Bootstrap は表題(caption)を表の下に置くので、上に出すために caption-top を付けた
- badge ── 状態のバッジ。text-bg-success などで、背景色と、それに合う文字色が同時に決まる
- btn ── ボタン。btn-primary などの色の種類があるが、今回はブランドの赤にしたいので、custom.css で btn-brand を作った
Bootstrap 5.3 のボタンは、色を --bs-btn-bg などのCSS 変数(第4回)で持っています。だから、変数を上書きするだけで、新しい色のボタンが作れます。
.btn-brand {
--bs-btn-color: #fff;
--bs-btn-bg: #c0392b; /* ふだんの背景 */
--bs-btn-hover-bg: #a93226; /* マウスを乗せたとき */
…
}
Bootstrap の色の組み合わせのコントラスト比も、WCAG の式で計算しておきました。
| クラス | 文字 / 背景(実測) | コントラスト比 |
|---|---|---|
| text-bg-warning | #000000 / #ffc107 | 12.88 : 1 |
| text-bg-success | #ffffff / #198754 | 4.53 : 1 |
| text-bg-danger | #ffffff / #dc3545 | 4.53 : 1 |
| text-bg-secondary | #ffffff / #6c757d | 4.69 : 1 |
| btn-primary(参考) | #ffffff / #0d6efd | 4.50 : 1 |
| btn-brand(自前) | #ffffff / #c0392b | 5.44 : 1 |
どれも、ふつうの文字の目安 4.5:1 は満たしています(btn-primary はぎりぎり)。ただしバッジの文字は小さいので、色を自分で変えるときは、この数字が下がらないように気を付けましょう。
フォームも Bootstrap で
第3回の申請フォームも書き直しました。入力欄には form-control、選択肢には form-select、ラジオボタンとチェックボックスは form-check の組み合わせです。label と name の付け方は第3回のまま。HTML の正しさは、Bootstrap になっても変わりません。
<form action="/api/expenses" method="post" enctype="multipart/form-data">
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<fieldset class="col-md-7">
<legend class="h6 section-title">申請の内容</legend>
<label for="title" class="form-label">件名(必須)</label>
<input type="text" class="form-control" id="title" name="title" required maxlength="50">
<div class="invalid-feedback">件名を入力してください。</div>
</fieldset>
<fieldset class="col-md-5">
<legend class="h6 section-title">支払い方法</legend>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="payment" id="pay-personal" value="personal" checked>
<label class="form-check-label" for="pay-personal">立て替え(自分で払った)</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="payment" id="pay-corporate" value="corporate">
<label class="form-check-label" for="pay-corporate">会社のカード</label>
</div>
</fieldset>
</div>
</div>
</div>
…
<div class="form-check mb-4">
<input class="form-check-input" type="checkbox" id="agree" name="agree" value="yes" required>
<label class="form-check-label" for="agree">経費規程を確認しました(必須)</label>
</div>
<div class="d-flex flex-wrap justify-content-end gap-2">
<button type="submit" class="btn btn-outline-secondary" name="action" value="draft" formnovalidate>下書き保存</button>
<button type="submit" class="btn btn-brand" name="action" value="submit">申請する</button>
</div>
</form>
Bootstrap には、入力チェックの結果を色で示す見た目も用意されています。form に was-validated というクラスが付くと、ブラウザの検査(第3回の required など)の結果に合わせて、欄が赤や緑になります。
写真は、件名だけを入れて was-validated を付けたところです。気になるのは、明細の2・3行目のような空の任意の欄にも、緑のチェックが付くこと。「入力していないのに OK?」と利用者を迷わせるので、実際には、入力した欄だけに印を付けるなどの工夫が要ります。そして、was-validated を付けるのは「送信ボタンが押されたとき」なので、JavaScript の仕事です。続きは第9回の入力チェックで。
書き直した HTML
Bootstrap 版の index.html です(表の行は1行目だけ表示)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>申請一覧 | 経費精算システム</title>
<link rel="stylesheet" href="vendor/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="css/custom.css">
</head>
<body class="bg-body-tertiary">
<header class="navbar navbar-expand-md navbar-dark brand-bar">
<div class="container">
<a class="navbar-brand fw-bold" href="/"><img src="img/logo.svg" alt="" width="32" height="32"> 経費精算システム</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="メニューを開く">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<nav aria-label="メイン">
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/">申請一覧</a></li>
<li class="nav-item"><a class="nav-link" href="approve.html">承認</a></li>
<li class="nav-item"><a class="nav-link" href="dashboard.html">ダッシュボード</a></li>
</ul>
</nav>
<p class="navbar-text mb-0 ms-md-auto">新井 太郎(申請者)</p>
</div>
</div>
</header>
<main class="container py-4">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
<h1 class="h3 mb-0">申請一覧</h1>
<a class="btn btn-brand" href="form.html">+ 新規申請</a>
</div>
<section class="mb-4">
<h2 class="h5 section-title mb-3">今月のまとめ</h2>
<ul class="row g-3 list-unstyled">
<li class="col-12 col-sm-6 col-lg-4"><div class="card h-100"><div class="card-body">今月の申請:4 件</div></div></li>
<li class="col-12 col-sm-6 col-lg-4"><div class="card h-100"><div class="card-body">承認待ちの金額:<strong class="d-block fs-4 text-danger">32,400 円</strong></div></div></li>
<li class="col-12 col-sm-6 col-lg-4"><div class="card h-100"><div class="card-body">差し戻し:1 件</div></div></li>
</ul>
</section>
<section>
<h2 class="h5 section-title mb-3">申請の一覧</h2>
<div class="table-responsive" tabindex="0" role="region" aria-label="申請の一覧">
<table class="table table-striped table-hover align-middle caption-top wide-table mb-0">
<caption>2026年10月の申請(新しい順)</caption>
<thead class="table-light">
<tr>
<th scope="col">No.</th>
<th scope="col">申請日</th>
<th scope="col">件名</th>
<th scope="col" class="text-end">金額(円)</th>
<th scope="col">状態</th>
</tr>
</thead>
<tbody>
<tr>
<td>0007</td>
<td class="text-nowrap"><time datetime="2026-10-16">2026-10-16</time></td>
<td><a href="detail.html">大阪出張(交通費・宿泊費)</a></td>
<td class="text-end text-nowrap">32,400</td>
<td><span class="badge rounded-pill text-bg-warning">申請中</span></td>
</tr>
…
</tbody>
</table>
</div>
</section>
</main>
<footer class="py-4 text-center text-body-secondary">
<small>© 2026 KIJI 経費精算システム(練習用)</small>
</footer>
<script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
</body>
</html>
そして、自前の CSS はこれだけです。
/* Bootstrap の後に読み込んで、ブランドの色だけを足す */
/* ヘッダーの帯の色 */
.brand-bar {
background-color: #1f4e6b;
}
/* ブランド色のボタン(Bootstrap のボタンの変数を上書き) */
.btn-brand {
--bs-btn-color: #fff;
--bs-btn-bg: #c0392b;
--bs-btn-border-color: #c0392b;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #a93226;
--bs-btn-hover-border-color: #a93226;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #922b21;
--bs-btn-active-border-color: #922b21;
--bs-btn-focus-shadow-rgb: 192, 57, 43;
font-weight: 700;
}
/* 見出しの左の赤い線(第4回と同じ) */
.section-title {
padding-left: 10px;
border-left: 4px solid #c0392b;
}
/* 表は、これより狭くしない(狭い画面では横スクロール) */
.wide-table {
min-width: 640px;
}
.line-items {
min-width: 720px;
}
どちらも Nu Html Checker でエラーはありませんでした。HTML が少し長くなり、CSS がほとんどなくなった。見た目の指定が、CSS ファイルから HTML のクラスへ引っ越した、と考えると分かりやすいでしょう。
よくある落とし穴
Bootstrap ならではのつまずきを、二つ実験しました。
# ① custom.css を Bootstrap より先に読み込んだとき(.btn-brand の計算後の値) color → rgb(33, 37, 41) (正しい順番なら rgb(255, 255, 255)) background-color → rgba(0, 0, 0, 0) 透明(正しい順番なら rgb(192, 57, 43)) # ② Bootstrap 4 の書き方が混ざったとき(Bootstrap 5.3.8 のページで) ml-auto(v4) → margin-left 0px 名前の左端 502px ms-md-auto(v5) → margin-left 316.484px 名前の左端 819px data-toggle(v4) → ボタンを押すと aria-expanded="false"、メニューの高さ 0px data-bs-toggle(v5) → ボタンを押すと aria-expanded="true"、メニューの高さ 160px
- 読み込む順番 ── Bootstrap の .btn も、自前の .btn-brand も、詳細度は同じ 0-1-0。同点なら後に書いたほうが勝つ(第4回)ので、custom.css を先に書くと、.btn の「背景は透明」が勝ってしまう。自前の CSS は必ず Bootstrap の後
- 版の違い ── Bootstrap 4 と 5 では、クラス名や属性の名前がいくつも変わった。古い書き方は、エラーも出さずにただ効かない
| Bootstrap 4 の書き方 | Bootstrap 5 の書き方 |
|---|---|
| ml-*、mr-*、pl-*、pr-* | ms-*、me-*、ps-*、pe-*(left/right → start/end) |
| text-left、float-right | text-start、float-end |
| data-toggle、data-target | data-bs-toggle、data-bs-target |
| custom-select | form-select |
| form-group | なくなった(mb-3 などの余白で代わりに) |
| sr-only | visually-hidden |
| jQuery が必要 | jQuery は不要に |
AI は、ネット上に大量に残る Bootstrap 4 時代の書き方を、5 のページに平気で混ぜてきます。ml-auto や data-toggle を見かけたら要注意。「使っている Bootstrap は 5.3」と最初に伝え、出てきたクラスは公式の資料で確かめるのが、いにしえ流の付き合い方です。
重さと「Bootstrap 顔」
便利な道具箱には、重さもあります。ファイルの大きさと、Chromium のカバレッジ(CSS のうち、そのページで実際に使われた部分を数える機能)で測った値です。
ファイルの大きさと、実際に使われた割合(実測)
申請一覧で使われたのは、bootstrap.min.css の 7.4% だけ。残りは、ほかの部品のためのルールです。転送のときは gzip で圧縮されて約31KB になり、一度読み込めばブラウザが覚えておくので、社内の業務システムなら大きな問題にはなりにくい重さです。ただ、「たった1つのボタンのために Bootstrap を丸ごと」は、少し考えたほうがよいでしょう。
新井でも先輩、Bootstrap で作った画面って、どこかで見たような顔になりません?
古井いわゆる「Bootstrap 顔」だな。業務システムなら、見慣れた顔はむしろ安心感になる。お客さま向けの看板ページなら、custom.css で色や形をしっかり変えるか、手書きを選ぶ。道具は、目的で選ぶんだ。
結 — 一枚まとめと次回予告
- Bootstrap は 名前の付いた CSS のルールの詰め合わせ+動きの JS。CSS を書く代わりに、クラスを選ぶ。
- 自前の CSS は 必ず Bootstrap の後に読み込む。同じ詳細度なら後に書いたほうが勝つ。
- ユーティリティは 何を・どこに・どの幅から・どれだけ(例:ms-md-auto)と読む。グリッドは12列、区切りは 576・768・992・1200・1400px。
- ボタンの色は CSS 変数の上書きで作れる。コントラスト比と、caption の位置などの初期値は自分で確かめる。
- Bootstrap 4 の書き方(ml-auto、data-toggle)は 5 では黙って効かない。版を確かめてから使う。
新井これで見た目は一通り完成ですね! ……でも、明細の合計金額、まだ自分で電卓をたたいて書いてるんですけど。
古井それを計算させるのが、次の筋肉だ。第7回からは JavaScript。まずは合計を出す関数から始めよう。
参考
- Bootstrap — Get started with Bootstrap (v5.3)
- Bootstrap — Breakpoints
- Bootstrap — Grid system
- Bootstrap — Spacing
- Bootstrap — Navbar
- Bootstrap — Buttons
- Bootstrap — Tables
- Bootstrap — Validation
- Bootstrap — Migrating to v5
- npm — bootstrap
- MDN — Subresource Integrity
- Chrome for Developers — Coverage: Find unused JavaScript and CSS
ページの写真は、筆者が作ったデモページを2026年10月17日にクラウド上の Chromium(Playwright)で撮影したものです。スマホは Playwright のスマホ表示(isMobile)による再現で、実機の画面ではありません。撮影環境には Hiragino Sans がないため、日本語は Noto Sans CJK JP で表示されています。Bootstrap は npm の bootstrap 5.3.8 のファイルを使い、版の公開日は npm の記録によります。ファイルの大きさ、カバレッジ、余白の段階、container の幅、カードの並び方、ボタンとバッジの色、読み込む順番と Bootstrap 4 の書き方の実験結果は、同じ Chromium で測った実際の値です。コントラスト比は WCAG 2.2 の式で計算しました。HTML と custom.css は Nu Html Checker(vnu)でエラーがないことを確かめています。登場人物は架空の人物です。
いにしえのフロントエンド入門 第6回 — CSS ③:Bootstrap / 2026.10