Old-School Front-End / 2026 ・ No.04
いにしえのフロントエンド入門
第4回 CSS ① ── セレクタとボックス:申請一覧に自前の CSS で服を着せる
公開
AI に頼まず、一行ずつ手で書く「いにしえの」フロントエンド入門。第4回から、いよいよ CSS です。
第2回で作った申請一覧に、ライブラリを使わず自分で書いた CSS で服を着せます。
覚えるのは、どの要素に当てるか(セレクタ)、ぶつかったらどれが勝つか(カスケード)、
そしてすべての要素は箱である(ボックスモデル)、の三つです。
今回のゴール
新井先輩、CSS も AI に書いてもらいました! ただ、ボタンの色を直したら表の色が崩れて、表を直したらボタンが崩れて……。最後に !important を付けたら、全部直りました!
古井その !important、いくつある?
新井……23個です。
古井火事のたびに、全部の部屋で消火器をまいたようなものだな。今日は、CSS が「どれを勝たせるか」をどう決めているのかを、手で書きながら覚えよう。
いまここ:第4回
今回は、第2回の申請一覧(index.html)に、css/style.css という CSS ファイルを付けます。HTML 側の変更は、CSS を読み込む1行と、いくつかの class だけ。見た目の変化は、この通りです。
①は CSS を読み込んでいない状態です。見出しが太く大きく、リンクが青く下線付きなのは、ブラウザが最初から持っている初期スタイルのおかげ。②は、そこに自分の CSS を重ねたものです。CSS は「HTML を書き換えずに、見た目だけを変える」ための言語なのです。
Vibe Coding では、CSS は一番「なんとなく動く」ものが積み上がりやすい場所です。思った通りにならない理由が見えないまま、指定を足して上書きしていく。今回は、その理由を自分で読めるようになることが目標です。
CSS の付け方
CSS を HTML に付ける方法は、三つあります。
CSS の付け方3種類
本番は③です。head の中に link 要素を1行書きます。
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>申請一覧 | 経費精算システム</title> <link rel="stylesheet" href="css/style.css"> </head>
href は、この HTML ファイルから見た場所(相対パス)です。第1回で決めたフォルダの形で言うと、こうなります。
expense/
├─ index.html ← ここから見て
├─ form.html
├─ css/
│ └─ style.css ← css フォルダの style.css
└─ img/
└─ logo.svg
CSS を1枚のファイルにまとめておけば、form.html も、これから作る承認の画面も、同じ link を書くだけで同じ見た目になります。ボタンの色を変えたくなったら、直すのは style.css の1か所だけです。
古井CSS が広まる前は、<font color="red"> と、文字の一つ一つに色を書いていたもんだ。
新井色を変えるときは……。
古井全ページを開いて、全部書き換える。CSS が提案されたのは1994年10月、W3C の勧告(CSS1)になったのは1996年12月。中身と見た目を分けるのは、いにしえの人たちが苦労の末に手に入れた知恵なんだ。
ルールの形
CSS は、ルール(規則)を並べて書きます。一つのルールの形は、これだけです。
CSS のルールを分解する
「どの要素に(セレクタ)」「何を(プロパティ)」「どうする(値)」。日本語にすると「まとめの中の li の、幅を、220px にする」です。
CSS は、間違いがあっても何も言わずに無視します。たとえば color: red の後ろの ; を忘れて次の行に background: #fff; と続けると、ブラウザには「color: red background: #fff」という一つの宣言に見え、二つとも捨てられます。エラーの画面は出ません。「効かない」と思ったら、まず ; と { } の対応を疑いましょう。
セレクタ:どれに当てるか
セレクタは、HTML の木(第2回)の中から、要素を選ぶための書き方です。申請一覧の木の上で、選ばれる要素を確かめてみましょう。
セレクタが選ぶ要素
C の .summary li に注目してください。li はナビの中にもありますが、選ばれるのはまとめの中の li だけです。もし li { width: 220px; } と書いていたら、ナビのリンクまで 220px の幅になってしまいます。
| 書き方 | 名前 | 選ぶもの | 今回の使いどころ |
|---|---|---|---|
| h2 | 要素名(型) | その要素すべて | 見出しの左の赤い線 |
| .button | クラス | class にその名前を持つ要素 | リンクをボタン風に |
| #warn | ID | id がその名前の要素(ページに1つ) | 使わない(強すぎる。五章) |
| * | 全称 | すべての要素 | box-sizing の指定(六章) |
| .summary li | 子孫(空白) | 中にある要素(何段下でも) | まとめの箱 |
| body > header | 子(>) | すぐ下の要素だけ | ページ全体のヘッダー |
| th, td | グループ(,) | どちらか | 表のマスの余白 |
| a:hover、tr:nth-child(even) | 疑似クラス(:) | 状態や位置で選ぶ | マウスを乗せた行、偶数の行 |
使い分けの基本は、見た目を付けたい部品には class を付けて、class で選ぶこと。今回 HTML に足した class は、site、user、button、summary、amount、status の6種類です。名前は「見た目」ではなく「役割」で付けます。red-text ではなく amount。そうしておけば、色を青に変えても名前と中身が食い違いません。
新井ID のほうが一発で選べて、速そうですけど?
古井速さの問題じゃない。ID は強すぎるんだ。次の章で、その「強さ」の話をしよう。
ぶつかったら、どれが勝つか
一つの要素に、いくつものルールが当たることはよくあります。色の指定がぶつかったら、どれが勝つのか。これを決める仕組みが、CSS の名前の「C」、カスケード(Cascading = 段々に流れ落ちる)です。
詳細度と、勝負の決め方
セレクタには詳細度という3桁の点数があります。ID の数、クラス・疑似クラス・属性の数、要素名の数。比べるのは左の桁からで、ID が1つあれば、クラスをいくつ重ねても勝てません。
本当にこの通りに決まるのか、実験用のページを作って、ブラウザが計算した色を取り出してみました。
# 実験用の CSS
p { color: gray; }
.note { color: blue; }
p.note { color: green; }
#warn { color: red; }
.later { color: blue; }
.later { color: orange; }
.vip { color: teal !important; }
# 要素ごとに、ブラウザが計算した色(getComputedStyle)
<p>
→ gray rgb(128, 128, 128) p だけが当たる
<p class="note">
→ green rgb(0, 128, 0) p.note(0-1-1)が .note(0-1-0)に勝つ
<p id="warn" class="note">
→ red rgb(255, 0, 0) #warn(1-0-0)が勝つ
<p class="note" style="color: purple">
→ purple rgb(128, 0, 128) style 属性がセレクタに勝つ
<p class="later">
→ orange rgb(255, 165, 0) 同点なので、後に書いた .later
<span class="vip" style="color: purple">
→ teal rgb(0, 128, 128) !important が style 属性に勝つ
図の通りの結果です。2番は同じ「緑か青か」の勝負で p.note が勝ち、5番は同じ点数なので、後に書いた orange が勝ちました。6番は、!important が style 属性さえ押しのけたところです。
!important は、勝負の階段の一番上に飛び乗る指定です。一度使うと、それを上書きするにはまた !important が必要になり、点数の比べ合いが始まります。冒頭の「23個」は、まさにこの状態。AI が書いた CSS に !important や ID のセレクタ、style 属性が多いときは、勝ち負けを点数で殴り合って解決した跡だと思って読みましょう。手で書くときは、class 1つ(0-1-0)を基本に、詳細度を低くそろえるのが長持ちのこつです。
継承:親から受け継ぐもの
もう一つ、ルールが当たっていないのに値が決まる仕組みがあります。継承です。
# 親に color・border・font-family を指定。子の p には何も書かない
.parent { color: #1f4e6b; border: 3px solid #1f4e6b; font-family: serif; }
子の color → rgb(31, 78, 107)(受け継ぐ)
子の font-family → serif(受け継ぐ)
子の border-top-width → 0px(受け継がない)
文字の色や書体は子に受け継がれ、枠線は受け継がれません。文字に関わるもの(color、font-family、font-size、line-height など)は受け継ぎ、箱に関わるもの(border、padding、margin、background など)は受け継がない、と覚えておけばほぼ当たります。今回の CSS で、body に一度だけ文字の色と書体を書いているのは、この継承を利用するためです。
ボックスモデル:すべては箱
ブラウザは、すべての要素を四角い箱として並べます。箱は、内側から content(中身)、padding(内側の余白)、border(枠線)、margin(外側の余白)の4層です。まとめの箱を、実際の数字で描くとこうなります。
まとめの箱のボックスモデル
ここで大事なのが、width: 220px がどこからどこまでの幅かです。これは box-sizing で決まります。
content-box と border-box
初期値の content-box では、width は中身だけの幅です。padding と border を足すと、220px と書いたのに 254px の箱になります。「220px の箱を3つ並べたつもりが、はみ出した」は、昔からの定番の事故でした。そこで、今回の CSS の最初に、こう書いています。
*, *::before, *::after {
box-sizing: border-box;
}
すべての要素(*)と、その前後に付く疑似要素を border-box にする、よく使われる書き方です。これで「width は枠線までの幅」になり、計算が直感に合います。実験ページで測った値も見ておきましょう。
# ① width: 220px; padding: 12px 16px; border: 1px の箱の、実際の幅 box-sizing: content-box → 254px(220 + 16×2 + 1×2) box-sizing: border-box → 220px(中身が 186px に縮む) # ② margin 16px の段落のすぐ下に、margin 24px の段落 二つの段落のすき間 → 24px(16 + 24 = 40 ではない)
②はマージンの相殺です。縦に並んだ箱の上下の margin は、足し算されずに大きいほうだけが残ります。「余白が思ったより狭い」ときは、これを疑いましょう。
| display | 並び方 | width・上下の余白 | 今回の例 |
|---|---|---|---|
| block | 縦に積む(横幅いっぱい) | 効く | p、h2、section、ul(初期値) |
| inline | 文の中に流れる | width は効かない。上下の余白は周りを押し広げない | a、span、strong(初期値) |
| inline-block | 文のように横に並ぶ | 効く | .button、.summary li、.status、ナビの li |
a はもともと inline なので、そのままでは padding を付けてもボタンらしい四角になりません。display: inline-block にして「箱として扱って」と頼むのが、ボタン風のリンクのこつです。まとめの箱も inline-block で横に並べました(もっと便利な並べ方は、次回のレイアウトで)。
単位・色・変数
| 単位 | 意味 | 今回の使いどころ |
|---|---|---|
| px | 画面の点の大きさが基準 | 余白、枠線、箱の幅 |
| rem | html の文字の大きさが基準(実測で 16px。0.875rem = 14px) | 文字の大きさ |
| em | その要素の文字の大きさが基準 | (今回は使わない) |
| % | 親の大きさが基準 | 表の幅(width: 100%) |
文字の大きさを rem で書いておくと、利用者がブラウザの設定で文字を大きくしたときに、ページの文字もそれに合わせて大きくなります。色は #1f4e6b(赤・緑・青を16進数で)、rgb(31, 78, 107)、gray のような名前で書けます。
同じ色を何か所にも書くなら、CSS 変数(カスタムプロパティ)に名前を付けておきます。
:root {
--brand: #1f4e6b; /* 名前は -- で始める */
--accent: #c0392b;
}
body > header { background: var(--brand); } /* var() で呼び出す */
.button { background: var(--accent); }
:root はページの一番外側の要素(html)のこと。変数も継承されるので、ここに書けばページ中どこからでも使えます。会社の色が変わっても、直すのは1行です。
色を決めたら、文字と背景のコントラスト比を確かめます。アクセシビリティの指針 WCAG では、ふつうの文字は 4.5:1 以上が目安です。今回の組み合わせを、WCAG の計算式で計算しました。
| 場所 | 文字 / 背景 | コントラスト比 |
|---|---|---|
| 新規申請ボタン | #ffffff / #c0392b | 5.44 : 1 |
| ヘッダー | #ffffff / #1f4e6b | 8.9 : 1 |
| 表の見出しの説明(caption) | #5b6b75 / #f4f6f9 | 5.1 : 1 |
| 申請中 | #8a5a00 / #fff4d6 | 5.41 : 1 |
| 承認済み | #1d6b3a / #e3f4e9 | 5.72 : 1 |
| 差し戻し | #a93226 / #fdecea | 5.79 : 1 |
| 精算済み | #4d5b66 / #eceff2 | 6.06 : 1 |
状態のバッジは、色分けしたうえで文字も残しています。色の見え方は人によって違うので、「赤いから差し戻し」のように色だけで意味を伝えないのも WCAG の決まりの一つです。
申請一覧を仕上げる
ここまでの道具で、申請一覧を仕上げます。HTML 側の変更点を抜き出すと、これだけです。
…
<link rel="stylesheet" href="css/style.css">
…
<p class="site"><a href="/"><img src="img/logo.svg" alt="" width="32" height="32"> 経費精算システム</a></p>
…
<p class="user">新井 太郎(申請者)</p>
…
<p><a class="button" href="form.html">+ 新規申請</a></p>
…
<ul class="summary">
…
<th scope="col" class="amount">金額(円)</th>
…
<td class="amount">32,400</td>
<td><span class="status status-submitted">申請中</span></td>
…
そして、css/style.css の全体です。上から「全体の約束 → ヘッダー → 本体 → 表 → フッター」の順に、ページの上から下へ並べています。
/* ===== 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: 24px;
}
h1 {
margin: 0 0 16px;
font-size: 1.5rem;
}
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;
}
/* まとめ:小さな箱を横に並べる */
.summary {
list-style: none;
margin: 0;
padding: 0;
}
.summary li {
display: inline-block;
width: 220px;
margin: 0 12px 12px 0;
padding: 12px 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
vertical-align: top;
}
.summary strong {
display: block;
color: var(--accent);
font-size: 1.25rem;
}
/* ===== 4. 表 ===== */
table {
width: 100%;
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;
}
/* 状態のバッジ(色だけに頼らず、文字も残す) */
.status {
display: inline-block;
padding: 2px 10px;
border: 1px solid;
border-radius: 999px;
font-size: 0.8125rem;
font-weight: 700;
}
.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;
}
- body > header ── ページ全体のヘッダーだけを選ぶ。section の中に header を書くことがあっても、巻き込まない
- h2 の border-left ── 左だけに太い線を引いて、見出しの目印に。border は上下左右を別々に指定できる
- border-collapse: collapse ── 表のマスの枠線を1本にまとめる。これがないと、隣のマスとの間に線が2本並ぶ
- tbody tr:nth-child(even) ── 偶数の行だけを薄い色にする縞模様。横に長い行を目で追いやすくなる
- .amount ── 金額は右寄せにして、font-variant-numeric: tabular-nums で数字の幅をそろえる。桁がそろうので、大きい金額がひと目で分かる
- .status-… ── 共通の形は .status に、色だけを .status-approved などに分けた。1つの要素に class を2つ付けている
新井!important、一つもないですね。
古井ID のセレクタも style 属性もない。点数の高いものを使っていないから、殴り合いが起きないんだ。
状態で変える::hover と :focus-visible
疑似クラスを使うと、「マウスが乗っているとき」「キーボードで選ばれているとき」のような状態に応じて見た目を変えられます。
tbody tr:hover {
background: #fdf2e9; /* マウスを乗せた行 */
}
.button:focus-visible {
outline: 3px solid #f5b041; /* キーボードで選んだとき */
outline-offset: 2px;
}
:focus-visible は、ブラウザが「枠を見せたほうがよい」と判断したフォーカスのときだけ当たります。Tab キーで移ったときは出て、マウスでクリックしたときはふつう出ません。
新井そのフォーカスの枠、ちょっと野暮ったくないですか? AI に聞いたら outline: none で消せるって。
古井マウスを使わず、キーボードだけで操作する人もいる。その人にとって、あの枠は「いまどこにいるか」を示す唯一の目印だ。
新井地図から現在地のピンを消すようなもの、ですか。
古井そうだ。消すなら、代わりの目印を必ず付ける。WCAG でも、フォーカスが見えることは決まりになっている。
「なぜこうなる?」を調べる
CSS が思った通りにならないとき、指定を足す前に、ブラウザに理由を聞きます。要素を右クリックして「検証」を選ぶと、開発者ツールが開きます。
- Styles(スタイル) ── その要素に当たっているルールが、勝った順に並ぶ。負けた宣言には取り消し線が引かれる。「どのルールに負けたのか」が一目で分かる
- Computed(計算済み) ── 最終的に決まった値の一覧。継承で決まった値もここで見える。ボックスモデルの図も表示される
- その場で書き換え ── 値をクリックすると、ファイルを直さずに試せる。うまくいったら、style.css に書き写す
書いた CSS の文法は、W3C の CSS 検証サービスか、第2回で使った Nu Html Checker(--css を付ける)で確かめられます。今回の style.css と index.html は、どちらもエラーなしでした。
AI に CSS を直してもらうときも、先に開発者ツールで「なぜ」を見るのがおすすめです。「この色は .note に負けている」と分かっていれば、AI への頼み方も「p.note の色を変えて」と具体的になり、!important の積み増しも防げます。理由が分かる人の Vibe Coding は、速いうえに崩れません。
結 — 一枚まとめと次回予告
- CSS は link 要素で別ファイルとして読み込む。中身(HTML)と見た目(CSS)を分ければ、直すのは1か所で済む。
- ルールは セレクタ { プロパティ: 値; }。間違いは黙って無視されるので、; と { } に気を付ける。
- セレクタは class を基本に。名前は見た目ではなく役割で付ける。子孫・子・疑似クラスで、選ぶ範囲を絞る。
- ぶつかったら !important → style 属性 → 詳細度 → 後に書いたものの順で勝負が決まる。詳細度を低くそろえ、!important に頼らない。文字の色や書体は継承される。
- すべての要素は content・padding・border・margin の箱。box-sizing: border-box で width を枠線までの幅にする。縦の margin は相殺される。
新井見違えましたね! さっそくスマホで開いて、課長に見せてきます。
古井……スマホで開く前に、次回を読んでおけ。第5回はレイアウトとスマホ対応。箱を並べる本当の道具を出すぞ。
参考
- MDN — CSS selectors
- MDN — Introduction to the CSS cascade
- MDN — Specificity
- MDN — !important
- MDN — Inheritance
- MDN — The box model
- MDN — box-sizing
- MDN — Mastering margin collapsing
- MDN — Using CSS custom properties (variables)
- MDN — :focus-visible
- MDN — font-variant-numeric
- W3C WAI — Understanding SC 1.4.3: Contrast (Minimum)
- W3C WAI — Understanding SC 1.4.1: Use of Color
- W3C WAI — Understanding SC 2.4.7: Focus Visible
- Chrome for Developers — Chrome DevTools
- W3C — CSS Validation Service
- W3C — Cascading Style Sheets, designing for the Web: The CSS saga
ページの写真は、筆者が作ったデモページを2026年10月17日にクラウド上の Chromium(Playwright)で撮影したものです。撮影環境には Hiragino Sans がないため、日本語は Noto Sans CJK JP で表示されています(お使いの環境では書体が異なります)。詳細度の実験の色は getComputedStyle()、箱の幅とすき間は getBoundingClientRect() で、同じ Chromium から取り出した実際の値です。コントラスト比は WCAG 2.2 の相対輝度の式で計算しました。index.html と style.css は Nu Html Checker(vnu)でエラーがないことを確かめています。CSS の歴史の年月は W3C の資料によります。登場人物は架空の人物です。
いにしえのフロントエンド入門 第4回 — CSS ①:セレクタとボックス / 2026.10