目次を開く(全 20 章)
01
CSS の書き方
CSS は「どれに(セレクタ)、何を(プロパティ)、どうする(値)」の 3 点セットです。これがひたすら並んでいるだけです。
h1 { /* セレクタ:どの要素に当てるか */
color: #2f80b8; /* プロパティ: 値; ← 最後のセミコロンを忘れずに */
font-size: 2rem;
}
/* これはコメント。CSS には // 形式のコメントはありません */
HTML への当て方は 3 通りありますが、基本は外部ファイル 1 択です。
- 外部ファイル(推奨)—
<link rel="stylesheet" href="…css">。全ページで共有でき、直す場所が 1 か所になります。 - <style> タグ — そのページだけの指定。プレビューや実験用。
- style 属性 —
<p style="color:red">。優先度が高すぎて後から上書きできなくなるので、原則使いません(JavaScript から動的に変えるときは別)。
02
セレクタ
「どれに当てるか」の指定方法です。まずはこれだけ覚えれば十分です。
p { } /* 要素:すべての p */
.card { } /* クラス:class="card" 何個あってもよい ← 主役 */
#header { } /* id:ページに 1 つだけ。強すぎるので控えめに */
[type="email"] { } /* 属性 */
.card p { } /* 子孫:card の中にある p すべて */
.card > p { } /* 直接の子だけ */
.card + p { } /* 直後に並ぶ兄弟 */
.card ~ p { } /* 後ろに続く兄弟すべて */
a:hover { } /* 状態:マウスが乗っている */
a:focus-visible { } /* キーボードで選ばれている(← 消してはいけない) */
li:first-child { } /* 位置 */
li:nth-child(2n){ } /* 偶数番目 */
input:checked { } /* チェックされている */
.card, .panel { } /* まとめて指定(カンマ区切り) */
見た目ではなく役割で名付けます。.red-text ではなく .error-message。あとで色を変えたときに、名前が嘘にならないためです。単語の区切りはハイフン(.site-header)が一般的です。
03
優先順位と継承
同じ要素に複数の指定が当たったとき、どれが勝つか。これが CSS でいちばん最初につまずくところです。ルールは「詳細度 → 書いた順」。詳細度が同じなら、後に書いたほうが勝ちます。
| 種類 | 例 | 強さ |
|---|---|---|
| style 属性 | style="color:red" | 非常に強い(使わない) |
| id | #header | 強い |
| クラス・属性・擬似クラス | .card :hover | ふつう ← ここで戦う |
| 要素 | p div | 弱い |
:where() の中身 | :where(.card) | ゼロ(上書きしやすい) |
!important は全部を飛び越えて勝ちますが、次に上書きしたくなったとき
もっと強い !important が必要になるという消耗戦を招きます。効かないときは「詳細度が足りない」のか「順番が逆」なのかを、ブラウザの開発者ツールで確かめてください。
@layer を使うと、この戦い自体を避けられます。
なお color や font-family などは親から子へ引き継がれます(継承)。
body に一度だけ書けば全体に効くので、同じ指定を繰り返さないで済みます。一方 border や padding は継承しません。
04
リセット CSS — 土台をそろえる
ブラウザは、何も指定しなくても見出しや段落に独自の余白を付けています。しかもその値はブラウザごとに微妙に違います。 最初にこれを一度そろえてしまうのがリセット CSS です。 CSS ファイルのいちばん上に置きます。
/* 1. 幅の計算に padding と border を含める */
*, *::before, *::after { box-sizing: border-box; }
/* 2. 既定の余白を消す(必要な余白は自分で付ける) */
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol, pre {
margin: 0;
}
/* 3. 画像・動画を扱いやすくする */
img, svg, video, canvas {
display: block; /* 下にできる謎の隙間を消す */
max-inline-size: 100%; /* 親からはみ出さない */
}
/* 4. 本文の土台 */
body {
min-block-size: 100dvh;
line-height: 1.7;
font-family: system-ui, sans-serif;
-webkit-text-size-adjust: 100%; /* iPhone の横向きで文字が太らないように */
}
/* 5. フォーム部品は文字の設定を引き継がないので、明示する */
button, input, select, textarea {
font: inherit;
color: inherit;
}
/* 6. 記号を消すのは「箇条書きに見せない」ときだけ。
読み上げソフトにリストだと伝えるため role="list" を付ける */
ul[role="list"], ol[role="list"] {
list-style: none;
padding-inline-start: 0;
}
:where(h1, h2, p) { margin: 0; } のように書くと詳細度がゼロになり、あとから書くどんな指定でも上書きできます。リセットは「土台」なので、後から必ず負けるようにしておくのが理想です。
@layer と組み合わせると、さらに確実になります。
先達のリセットを借りる
自分で書かなくても、よくできたものが公開されています。ブラウザ間の差だけを埋める normalize.css、既定をしっかり消してしまう
@acab/reset.css(約 2.3KB、ほぼ全部が :where() 包み)など、方針の違うものがいくつもあります。使ってみるのも良いと思います。
どこまで消すかは製品ごとに違うので、一度中身を読んでから採用してください。
/* @layer と組み合わせると、リセットを「いちばん弱い層」に閉じ込められる。
@layer の宣言だけは @import より前に置けます */
@layer reset, base, components, utilities;
@import url("https://cdn.jsdelivr.net/npm/@acab/reset.css@0.11.0/index.min.css") layer(reset);
@0.11.0 のようにバージョンを固定すること。そして CDN からの読み込みはオフラインでは効かないので、公開するときはファイルをダウンロードして同梱するのが確実です(このサイトが CDN を一切使っていないのも同じ理由です)。
05
ボックスモデル
すべての要素は四角い箱です。内側から中身 → padding(内側の余白)→ border(線)→ margin(外側の余白)の順に重なっています。
.box {
inline-size: 220px; /* = width。文字の流れに沿った「横幅」 */
padding: 14px;
border: 4px solid #2f80b8;
margin-block-end: 12px; /* = margin-bottom */
background-color: #eaf2f8;
border-radius: 8px;
}
/* padding と border を幅に含める。これを最初に全体へ書いておく */
.fix {
box-sizing: border-box;
background-color: #fff2ec;
border-color: #ff6a3d;
}
上の箱は 220 + 14×2 + 4×2 = 256px になります。下の箱は指定どおり 220px。だから最初に全要素へ box-sizing: border-box を書いておくのが定番です。
*, *::before, *::after { box-sizing: border-box; }
width / height / margin-left の代わりに
inline-size / block-size / margin-inline-start と書く方法があります。縦書きや右から左に読む言語でも意味が崩れないので、新しく書くならこちらがおすすめです。
top/right/bottom/left は inset-* にまとめられます。
06
色の指定
色の書き方はいくつもありますが、今から覚えるなら
16 進数とスペース区切りの rgb()、そしてカスタムプロパティの 3 つで足ります。
currentColor は文字色をそのまま使えます
:root {
/* カスタムプロパティ(変数)。色は必ずここに集める */
--brand: #2f80b8;
--accent: #ff6a3d;
}
.row { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
padding: 8px 14px;
border-radius: 999px;
color: #fff;
font-size: 13px;
}
.a { background-color: var(--brand); }
/* 今の書き方はカンマなし。透明度は / のあとに書く */
.b { background-color: rgb(58 167 109); }
.c { background-color: rgb(255 106 61 / 45%); color: #14202b; }
/* 2 色を混ぜる。濃淡のバリエーションを手で計算しなくてよい */
.d { background-color: color-mix(in srgb, var(--brand) 50%, var(--accent)); }
.lead-text { color: var(--accent); }
.dot {
display: inline-block;
inline-size: 14px;
block-size: 14px;
border-radius: 50%;
background-color: currentColor; /* 文字色をそのまま使う */
vertical-align: -2px;
}
- カスタムプロパティは
--名前: 値;で定義し、var(--名前)で使います。:rootに書けばページ全体で使えます。 - 古い
rgba(255, 106, 61, 0.45)も動きますが、今はrgb(255 106 61 / 45%)に統一するとすっきりします。 hsl(210 60% 45%)は「色相・鮮やかさ・明るさ」で指定する書き方。明度だけ変えた色を作りやすいのが利点です。- 文字色と背景色のコントラストは必ず確認を。薄いグレーの文字は読めません。開発者ツールで比率を測れます。
07
配色を決める
色は増やすほど画面が散らかります。最初は主役・補助・差し色・文字の 4 つだけ決め、名前は「青」ではなく役割で付けます(あとで色を変えても名前が嘘になりません)。使う面積の目安は昔から 70 : 25 : 5 と言われます。
- 地色 … 約 70% — 白や
--sub-color。いちばん広く占める背景です --main-color… 約 25% — 見出し、ボタン、リンク、罫線--accent-color… 約 5% — いちばん押してほしい 1 か所だけ
注意したいのは、70% は「地色」であり主役の色ではないことです。主役を 70% 使うと画面が塗りつぶされ、かえって主役が目立たなくなります。なお --text-color は面積比とは別枠で、読みやすさ(背景とのコントラスト)で決めます。
そして、ホバーで少し濃く・無効状態で薄くといった派生色はcolor-mix() を使うと便利です。主役の色を変えたときには濃淡も自動で決まります。混色方法は in srgb より in oklab のほうが、明るさの変化が目で見て自然になります。
主役の色を 12% だけ混ぜた背景
:root {
--main-color: #2f80b8;
--sub-color: #eaf2f8;
--accent-color: #ff6a3d;
--text-color: #14202b;
}
.swatches { display: flex; flex-wrap: wrap; gap: 8px; }
.sw { padding: 10px 16px; border-radius: 8px; color: #fff; font-size: 12px; }
.main { background-color: var(--main-color); }
.sub { background-color: var(--sub-color); color: var(--text-color); }
.accent { background-color: var(--accent-color); }
.text { background-color: var(--text-color); }
.button {
padding: 9px 18px;
border: 0;
border-radius: 999px;
background-color: var(--main-color);
color: #fff;
font: inherit;
cursor: pointer;
transition: background-color .2s;
}
/* 黒を 18% 混ぜて、少しだけ濃くする */
.button:hover { background-color: color-mix(in oklab, var(--main-color) 82%, black); }
/* 押した瞬間はもう少し濃く */
.button:active { background-color: color-mix(in oklab, var(--main-color) 70%, black); }
/* 白を混ぜれば薄くなる */
.button:disabled {
background-color: color-mix(in oklab, var(--main-color) 40%, white);
cursor: not-allowed;
}
/* transparent を混ぜると半透明も作れる */
.panel {
margin-block-start: 12px;
padding: 12px;
border-radius: 8px;
background-color: color-mix(in srgb, var(--main-color) 12%, transparent);
color: var(--text-color);
}
--main-color の 1 行を別の色に書き換えて「↻ 再表示」を押すと、ホバー時も無効時も背景パネルも、まとめて追随します。
--main-dark のように変数へ入れておくと、その値は宣言した場所(:root)で確定します。子要素で --main-color だけ差し替えても追随しません。テーマごとに色を変えるなら濃淡も同じ場所で定義し直すか、上の例のように使う場所で直接混ぜてください。
08
グラデーション
グラデーションは「色」ではなく画像として扱われます。だから background-image に指定し、カンマ区切りで何枚でも重ねられます(先に書いたものが上)。写真の上に文字を載せるときの定番手段でもあります。
文字そのものを塗ることもできます
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; }
.g {
display: grid;
place-items: center;
block-size: 70px;
border-radius: 8px;
color: #fff;
font-size: 12px;
text-shadow: 0 1px 3px rgb(0 0 0 / 45%);
}
/* 直線状。角度 → 色 → 色。位置(%)で切り替え点を指定できる */
.linear { background-image: linear-gradient(120deg, #1b4e77, #2f80b8 60%, #ff6a3d); }
/* 放射状。中心の位置も指定できる */
.radial { background-image: radial-gradient(circle at 30% 30%, #ffffff, #2f80b8); }
/* 円錐状。円グラフや色相環に使える */
.conic {
background-image: conic-gradient(
#2f80b8 0turn 0.25turn,
#9dc6e2 0.25turn 0.5turn,
#ff6a3d 0.5turn 0.75turn,
#ffd9cd 0.75turn 1turn);
}
/* 繰り返すとストライプになる */
.stripe {
background-image: repeating-linear-gradient(45deg, #1b4e77 0 8px, #2f80b8 8px 16px);
}
/* 写真の上に「暗くする層」を重ねる。文字が確実に読めるようになる */
.overlay {
display: grid;
place-items: center;
block-size: 90px;
margin-block-start: 10px;
border-radius: 10px;
background-image:
linear-gradient(rgb(13 35 56 / 72%), rgb(13 35 56 / 30%)),
linear-gradient(120deg, #ffcc00, #3aa76d); /* ← ここが本来は写真 */
background-size: cover;
}
.overlay span { color: #fff; font-weight: 700; }
/* 文字だけをグラデーションで塗る */
.title {
margin-block-start: 10px;
font-size: 20px;
font-weight: 700;
background-image: linear-gradient(90deg, #2f80b8, #ff6a3d);
background-clip: text;
color: transparent;
}
色の切り替え点は #2f80b8 60% のように「色 位置」で書きます。位置を 2 つ書く(#1b4e77 0 8px)と、その区間はべた塗りになり、境目のはっきりした縞になります。
09
線と影と角丸
border と box-shadow は、今でも毎日使う道具です。「浮いている」「囲まれている」という手がかりは、平らなデザインが主流の今こそ効きます。一方 text-shadow は出番が減りましたが、写真の上に文字を置くときには今も有効です。
.card {
/* 太さ 種類 色 の順。色キーワードは red / blue のようにそのまま書く
(#red という書き方はありません。16 進数なら #f00) */
border: 1px solid #cfdde8;
border-radius: 12px;
padding: 14px;
margin-block-end: 10px;
/* x方向 y方向 ぼかし 広がり 色。
影の色は黒ではなく「濃い青を薄めたもの」にすると自然に見える */
box-shadow: 0 6px 18px -10px rgb(13 35 56 / 45%);
}
.float {
box-shadow: 0 14px 30px -14px rgb(13 35 56 / 55%);
border-color: transparent;
}
.dashed {
border: 2px dashed #9dc6e2; /* solid / dashed / dotted / none */
box-shadow: none;
}
/* 一辺だけ、角ひとつだけ、も指定できる */
.card.dashed { border-inline-start-width: 6px; border-start-start-radius: 0; }
.photo {
display: grid;
place-items: center;
block-size: 90px;
border-radius: 12px;
background-image: linear-gradient(120deg, #1b4e77, #2f80b8 60%, #ff6a3d);
}
.photo span {
color: #fff;
font-weight: 700;
/* 背景が明るくても文字が読めるように、うっすら影を敷く */
text-shadow: 0 2px 6px rgb(0 0 0 / 55%);
}
/* フォーカスの枠。box-shadow でリングを作る書き方もよく使う */
.focus-demo {
padding: 8px 16px;
border: 1px solid #cfdde8;
border-radius: 999px;
background-color: #fff;
}
.focus-demo:focus-visible {
outline: 2px solid transparent; /* Windows のハイコントラスト対策 */
box-shadow: 0 0 0 3px rgb(255 106 61 / 55%);
}
box-shadowは現役です。カードの浮き上がり、押したときの沈み込み、フォーカスリング。カンマ区切りでいくつも重ねられます。- 影は薄く、広く。
0 8px 24px -12pxのように 4 つめ(広がり)を負の値にすると、輪郭がぼやけて自然になります。真っ黒な影は安っぽく見えます。 text-shadowは写真の上だけ。本文に使うと読みにくくなります。写真の上なら、影より暗いグラデーションを 1 枚重ねるほうが確実です(次章)。outlineは消さない。キーボード操作の人が現在地を見失います。デザインが気に入らないなら、:focus-visibleで自分好みの枠に置き換えてください。border-radius: 50%で円、999pxで角丸のカプセル形。個別ならborder-start-start-radiusなどで 1 か所ずつ。
10
文字と単位
本文の読みやすさは、文字サイズ・行間・1 行の長さでほぼ決まります。凝った装飾より先にここを整えます。
読みやすい本文の作り方
行の長さは全角 30〜40 文字くらいに収めると、目が次の行を見つけやすくなります。行間は 1.7 前後。文字を大きくするより、まず余白と行間を整えるほうが効きます。
body {
font-family: "Hiragino Sans", "Noto Sans JP", system-ui, sans-serif;
line-height: 1.8; /* 単位なしで指定するのが基本 */
color: #14202b;
}
h2 {
/* 画面幅に応じて 1.2rem 〜 1.9rem の間でなめらかに変わる */
font-size: clamp(1.2rem, 1rem + 2vw, 1.9rem);
line-height: 1.35;
text-wrap: balance; /* 見出しの折り返しを整える */
}
p {
max-inline-size: 34em; /* 1 行が長くなりすぎないように */
text-wrap: pretty; /* 最終行に 1 文字だけ残さない */
}
単位の使い分け
| 単位 | 意味 | 主な使いどころ |
|---|---|---|
rem | ルートの文字サイズ基準(既定 16px) | 文字サイズ・余白。まずこれ |
em | その要素の文字サイズ基準 | 文字に連動させたい余白 |
px | 画面のドット | 線幅・角丸など、拡大したくないもの |
% | 親要素に対する割合 | 幅 |
vw / dvh | 画面幅・画面高さの 1% | 全画面の見出しなど。dvh はスマホのアドレスバー対応 |
ch | 文字「0」の幅 | 1 行の文字数を決めたいとき |
fr | 余った空間の分け前 | Grid 専用 |
clamp(最小, 可変, 最大) と書くと、画面幅に応じてなめらかに変わり、小さくなりすぎも大きくなりすぎもしません。メディアクエリで文字サイズを何段階も刻む必要がなくなります。
11
Flexbox — 一列に並べる
1 方向に並べるならこれ。ヘッダーの左右振り分け、ボタンの横並び、カードの列。
display: flex と gap を覚えるだけで、Web の 8 割のレイアウトが片付きます。
.bar {
display: flex;
align-items: center; /* 縦方向の位置をそろえる */
justify-content: space-between; /* 左右の端に寄せる */
gap: 12px;
padding: 10px 14px;
background-color: #eaf2f8;
border-radius: 8px;
}
.menu {
display: flex;
flex-wrap: wrap; /* 入りきらなければ折り返す */
gap: 4px 14px;
}
.menu a { color: #1b4e77; }
.cards {
display: flex;
gap: 10px;
margin-block-start: 12px;
}
.card {
flex: 1; /* 余った幅を等しく分け合う */
padding: 16px 12px;
background-color: #2f80b8;
color: #fff;
border-radius: 8px;
text-align: center;
}
.wide { flex: 2; } /* こちらは 2 倍もらう */
すき間は gap で空ける
並べたものの間隔は、margin ではなく gap で指定します。gap は要素と要素の間だけに効くので、いちばん外側に余分な余白が残りません。margin で空けていた頃は、最後のひとつだけ :last-child で打ち消す、といった後始末が必要でした。
/* すき間は 1 か所で決める。両端には余白が付かない */
.row {
display: flex;
gap: 16px;
}
/* 値を 2 つ書くと「行の間 列の間」の順になる */
.tags {
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
}
/* 個別に書きたいときは row-gap / column-gap */
.list {
display: flex;
row-gap: 8px;
column-gap: 24px;
}
gap が効くのは flex・grid・段組みの中だけです。ふつうに縦に積んだブロックの間隔は、これまでどおり margin で空けます。
flex-wrap: wrap と gap を組み合わせると、すき間の分だけ子要素の置ける幅が減ります。flex: 1 は残りを分け合うだけなので気になりませんが、width: 50% のように固定で書くと合計が幅を超えて 2 つ目が折り返してしまいます。calc(50% - 8px) のように引くか、次に出てくる Grid の 1fr に任せてください。
- display: flex
- 直接の子要素を並べる。既定は横方向。
- flex-direction
row(横)かcolumn(縦)。- gap
- 要素の間隔。margin で調整する時代は終わりました。
- justify-content
- 並びの方向の配置。
flex-start / center / space-between。 - align-items
- 交差方向(横並びなら縦)の配置。
centerで高さがそろいます。 - flex-wrap
wrapで折り返す。狭い画面対策の基本。- flex: 1
- 余白の分け前。数字が大きいほど多くもらいます。
12
CSS Grid — 格子に並べる
縦と横を同時に決めたいときは Grid。カード一覧やページ全体の段組みが得意です。メディアクエリを書かずに折り返す書き方から覚えると、すぐ役に立ちます。
/* 「90px 以上を保ちつつ、入るだけ並べて余りは等分」
これだけで、幅に応じて 2 列にも 4 列にもなる */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 8px;
}
.cell {
padding: 18px 0;
background-color: #9dc6e2;
border-radius: 6px;
text-align: center;
}
/* 場所に名前を付けて配置する書き方 */
.page {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-areas:
"side main"
"foot foot";
gap: 8px;
margin-block-start: 12px;
}
.side { grid-area: side; background-color: #ffd9cd; }
.main { grid-area: main; background-color: #ff6a3d; color: #fff; }
.foot { grid-area: foot; background-color: #0d2338; color: #fff; }
.page > * {
padding: 14px;
border-radius: 6px;
text-align: center;
}
ブラウザの幅を変えると、上の 6 個が自動で折り返します。auto-fill を auto-fit にすると、要素が少ないときに横いっぱいまで伸びます。
Grid の gap
書き方は Flex とまったく同じで、gap ひとつで縦横のすき間が決まります。ちがうのは 1fr との関係です。Grid はすき間を先に取り分けてから、残りを 1fr で分けるので、gap をいくら広げても列がはみ出しません。幅を % で計算していた頃といちばん違うところです。
/* 3 列 + すき間 16px。1fr は「すき間を引いた残り」を 3 等分するので、
合計が親の幅を超えることがない */
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
/* 縦と横で変えるなら「行の間 列の間」の順 */
.photos {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 24px 16px;
}
並べたいものが 1 方向なら Flex、行と列の両方を決めたいなら Grid。 中身の大きさに任せたいなら Flex、外側から枠を決めたいなら Grid、とも言えます。両方を入れ子で使うのがふつうです。
13
配置と重なり
通常の要素は上から順に積まれます(これを「通常フロー」と呼びます)。そこから外して自由に置きたいときだけ position を使います。
スクロールしてもくっつく見出し
この枠の中をスクロールしてみてください。
見出しが上端に貼り付いたまま残ります。
position: sticky は、通常フローのまま「ここまで来たら止まる」を指定できます。
ページ全体のヘッダー固定にもよく使われます。
下までスクロールすると、親要素の終わりで一緒に流れていきます。
.scroller {
block-size: 190px;
overflow: auto;
padding: 0 12px 12px;
border: 1px solid #cfdde8;
border-radius: 8px;
}
.sticky {
position: sticky; /* 通常フローのまま、端で止まる */
inset-block-start: 0; /* = top: 0 */
margin: 0;
padding: 10px 0;
background-color: #fff;
border-block-end: 2px solid #2f80b8;
font-size: 15px;
}
.badge-box {
position: relative; /* 中の absolute の基準になる */
margin-block-start: 12px;
padding: 18px 14px;
background-color: #eaf2f8;
border-radius: 8px;
}
.badge {
position: absolute; /* 基準からの位置で置く */
inset-block-start: -8px;
inset-inline-end: -6px;
padding: 3px 10px;
background-color: #ff6a3d;
color: #fff;
font-size: 11px;
border-radius: 999px;
}
- static
- 既定。通常フローのまま。
- relative
- 元の位置を保ったまま少しずらせる。absolute の基準にもなる。
- absolute
- フローから外れ、いちばん近い
relativeな祖先を基準に置かれる。 - fixed
- 画面に固定。スクロールしても動かない。
- sticky
- ふだんは通常フロー、指定位置まで来ると貼り付く。
inset-block-startの指定が必須。 - z-index
- 重なりの順番。
positionがstatic以外の要素にだけ効きます。
14
:is() :where() :has()
セレクタを短く書ける :is()、詳細度をゼロにする :where()、そして「中身によって親の見た目を変える」という長年できなかったことを可能にした :has()。この 3 つはどのブラウザでも使えます。
/* :is() — 長い列挙をまとめる。詳細度は中身のいちばん強いものになる */
:is(h1, h2, h3) + p { margin-block-start: .5em; }
/* 昔はこう書いていた */
.article h1 + p, .article h2 + p, .article h3 + p { }
/* :where() — 同じくまとめるが、詳細度は 0。
「あとから簡単に上書きできる土台」を作るのに向く */
:where(ul, ol) { padding-inline-start: 1.2em; }
/* :has() — 中に◯◯を持つ親、を選べる(親セレクタ) */
.card:has(img) { padding: 0; } /* 画像入りのカードだけ */
label:has(:checked) { background-color: #e6f4ec; }
form:has(:invalid) .send { opacity: .5; } /* 未入力なら送信を薄く */
おすすめ商品
NEWバッジ付き。
通常商品
バッジなし。
.field {
display: block;
padding: 10px 12px;
border: 1px solid #cfdde8;
border-radius: 8px;
transition: background-color .2s;
}
/* チェックされた input を「持つ」label の色を変える */
.field:has(:checked) {
background-color: #e6f4ec;
border-color: #3aa76d;
color: #1b6b45;
}
.card {
position: relative;
margin-block-start: 10px;
padding: 12px 14px;
border: 2px solid #cfdde8;
border-radius: 8px;
}
.card h3 { margin: 0 0 4px; font-size: 15px; }
.card p { margin: 0; font-size: 13px; }
/* .badge を持つカードだけ色を変える */
.card:has(.badge) {
border-color: #ff6a3d;
background-color: #fff5f1;
}
.badge {
position: absolute;
inset-block-start: 10px;
inset-inline-end: 12px;
padding: 2px 8px;
background-color: #ff6a3d;
color: #fff;
font-size: 11px;
border-radius: 999px;
}
チェックボックスを押してみてください。JavaScript を 1 行も書かずに、親要素の見た目が変わります。
15
@layer とネスト記法
CSS が育ってくると「なぜか上書きできない」が必ず起きます。
@layer は優先順位をファイルの先頭で宣言してしまう仕組みで、この問題を根本から避けられます。
/* 先に順番を宣言する。後ろの層ほど強い */
@layer reset, base, components, utilities;
@layer reset {
/* :where() で詳細度 0 にしておくと、あとで何でも上書きできる */
:where(h1, h2, p) { margin: 0; }
}
@layer components {
/* id セレクタでも、utilities 層には勝てない */
#main .button { background-color: #2f80b8; }
}
@layer utilities {
.is-danger { background-color: #c0392b; } /* こちらが勝つ */
}
層の順番が詳細度より優先されます。 だから「コンポーネントの指定が、あとから足したユーティリティに勝ってしまう」といった事故が起きません。
ネスト記法
セレクタの繰り返しを書かずに済む書き方です。Sass のような別ツールを入れなくても、素の CSS で使えます。
子孫セレクタを書くだけなら & は要りません。
&(自分自身を指す記号)が必要なのは、次の 3 つの場合だけです。
- 擬似クラス・擬似要素を付ける —
&:hover&::before - 自分自身にクラスや属性を足す —
&.is-open&[disabled] - 親を後ろに置きたい —
.sidebar &(=.sidebar .card)
.card {
padding: 16px; /* 宣言はネストしたルールより先に書く */
border-radius: 12px;
h3 { font-size: 1.1rem; } /* .card h3 と同じ。& は書かなくてよい */
p + p { margin-block-start: .5em; }
&:hover { translate: 0 -4px; } /* .card:hover */
&.is-open { border-color: #ff6a3d; } /* .card.is-open */
.sidebar & { padding: 12px; } /* .sidebar .card(親を後ろに置く) */
@media (width >= 40rem) { /* メディアクエリも中に書ける */
padding: 24px;
}
}
ネストが最初に実装されたころは、ネストしたセレクタの先頭に & か記号が必須でした。
h3 { } のように要素名で始められるようになったのは、そのあと緩和されてからです。そのため既存のコードには & 付きが多く残っています。動作は同じなので、読むときは「これは省略できるほうだな」と思って構いません。
プロパティの宣言は、ネストしたルールより前に書いてください。 後ろに書くとブラウザによって解釈が揺れます。また、入れ子は深くしすぎないこと。3 段を超えると、どの要素の話か分からなくなります。
16
レスポンシブ — 画面幅と、要素の幅
スマホから大画面まで、1 つの CSS で対応します。基準は「狭い画面の指定を先に書き、広くなったら足す」(モバイルファースト)。こうすると打ち消しの指定が減ります。
/* まず狭い画面の指定を書く */
.cards { display: grid; gap: 12px; }
/* 60rem(=960px)以上のときだけ 3 列にする */
@media (width >= 60rem) {
.cards { grid-template-columns: repeat(3, 1fr); }
}
/* 昔の書き方(今も動く)
@media (min-width: 960px) { }
*/
/* 範囲もそのまま書ける */
@media (40rem <= width < 60rem) { }
/* 画面幅以外にも聞ける */
@media (prefers-reduced-motion: reduce) { } /* 動きを減らす設定 */
@media (prefers-color-scheme: dark) { } /* ダークモード */
@media (pointer: coarse) { } /* 指で操作している */
@media print { } /* 印刷時 */
コンテナクエリ — 「置かれた場所の幅」で変える
同じカードでも、サイドバーに置けば狭く、メインに置けば広い。画面幅ではなく親の幅で切り替えられるのがコンテナクエリです。部品を「どこに置いても正しく見える」ようにできます。
↓ 枠の右下をドラッグして幅を変えてみてください
幅で形が変わるカード
広いときだけ、タグと本文が横並びになります。
.holder {
container-type: inline-size; /* この要素を「幅の基準」にする */
container-name: card;
resize: horizontal; /* 手でつまんで幅を変えられるように */
overflow: auto;
min-inline-size: 180px;
max-inline-size: 100%;
padding: 10px;
border: 2px dashed #9dc6e2;
border-radius: 10px;
}
.card {
display: grid;
gap: 8px;
padding: 12px;
background-color: #eaf2f8;
border-radius: 8px;
}
.tag {
justify-self: start;
padding: 2px 10px;
background-color: #2f80b8;
color: #fff;
font-size: 11px;
border-radius: 999px;
}
.card h3 { margin: 0; font-size: 15px; }
.card p { margin: 0; font-size: 13px; }
/* 基準にした .holder の幅が 24rem 以上になったら、
その「子孫」である .card の並びを変える */
@container card (width >= 24rem) {
.card { grid-template-columns: auto 1fr; align-items: center; }
}
.hint { margin: 0 0 8px; font-size: 12px; color: #5b6f80; }
container-type を付けた要素自身のスタイルは、その @container では変えられません(自分の幅で自分を変えると堂々巡りになるため)。変えられるのは子孫だけです。だから上の例では、外側に .holder を 1 枚かぶせています。
17
ダークモード
色をすべてカスタムプロパティにまとめておけば、ダークモード対応は「変数を差し替えるだけ」で済みます。個別の場所で色を直接書いていると、ここで必ず苦労します。
:root {
/* フォームやスクロールバーもまとめて暗くしてくれる */
color-scheme: light dark;
--bg: #ffffff;
--text: #14202b;
--line: rgb(13 35 56 / 14%);
}
/* OS がダークのとき(ユーザーが手動で light を選んでいなければ) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #101820;
--text: #e6eef5;
--line: rgb(157 198 226 / 18%);
}
}
/* ページ上のボタンで明示的にダークを選んだとき */
:root[data-theme="dark"] {
--bg: #101820;
--text: #e6eef5;
--line: rgb(157 198 226 / 18%);
}
body {
background-color: var(--bg);
color: var(--text);
}
「OS 設定に従う(既定)」「手動でライト」「手動でダーク」の 3 通りがあります。上のように :not([data-theme="light"]) を挟むと、
手動の選択が OS 設定に必ず勝つようになります。このページの右上のボタンも、まさにこの仕組みで動いています。
18
動きをつける
状態が変わるときになめらかにするのが transition、
放っておいても動き続けるのが animation です。動かすのは opacity と transform 系にとどめると、なめらかに動きます。
読み込み中
.button {
padding: 10px 20px;
border: 0;
border-radius: 999px;
background-color: #2f80b8;
color: #fff;
font: inherit;
cursor: pointer;
/* 変化にかける時間と加減速 */
transition: background-color .25s ease, translate .25s ease;
}
.button:hover {
background-color: #ff6a3d;
translate: 0 -3px; /* transform: translateY(-3px) と同じ */
}
/* キーボードで選ばれたときの枠は消さない */
.button:focus-visible {
outline: 3px solid #ff6a3d;
outline-offset: 3px;
}
@keyframes blink {
from { opacity: 1; }
to { opacity: .2; }
}
.dot {
display: inline-block;
inline-size: 10px;
block-size: 10px;
border-radius: 50%;
background-color: #3aa76d;
animation: blink 1s ease-in-out infinite alternate;
}
/* 「動きを減らす」設定の人には動かさない。必ず入れる */
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
19
早見表:知っておくと助かるもの
| プロパティ | できること |
|---|---|
aspect-ratio: 16 / 9 | 縦横比を保つ。画像や動画の枠に |
object-fit: cover | 画像を歪めずに枠いっぱいに収める |
gap | Flex / Grid の間隔。margin より圧倒的に楽 |
min() max() clamp() | 計算して値を決める。min(100% - 2rem, 1100px) が定番 |
overflow-wrap: anywhere | 長い URL がはみ出すのを防ぐ |
text-wrap: balance / pretty | 見出しと本文の折り返しを整える |
scroll-margin-block-start | 固定ヘッダーの下にアンカーが隠れるのを防ぐ |
accent-color | チェックボックスやスライダーの色をブランド色に |
:focus-visible | キーボード操作のときだけ枠を出す。消さない |
filter / backdrop-filter | ぼかしや明度調整。背景をすりガラスに |
translate rotate scale | transform を個別に書ける新しい書き方 |
place-items: center | Grid の中央寄せがこれ 1 行で終わる |
@supports | その機能が使えるかを聞いてから適用する |
content-visibility: auto | 画面外の描画を後回しにして表示を速く |
横方向:inline-size: min(100% - 2rem, 1100px); margin-inline: auto;
上下左右:display: grid; place-items: center;
この 2 つを覚えておけば、中央揃えで悩むことはもうありません。
20
次は JavaScript へ
ここまでで、静かなページなら作れます。
:has() や <details> のおかげで、
昔は JavaScript が必要だったことの多くが CSS と HTML だけで済むようになりました。それでも足りないところ ── 計算する、データを取ってくる、状態を覚えておく ── を JavaScript が担当します。
実行結果がその場に出るので、書きながら確かめられます。