セレクタとカスケード — 結合子・疑似クラス・詳細度
同じ要素に効くスタイルが複数あるとき、どれが勝つのか。!important を付けたら直ったが理由は分からない。CSS で消耗しやすいのはこの領域です。
勝敗を決めているのは カスケード という仕組みで、その中心にあるのが 詳細度 です。詳細度は暗記ものに見えますが、規則は 3 つの列を左から比べるだけです。
この章で学ぶこと
- セレクタの種類と、結合子が要素の関係をどう表すか
- 子結合子と子孫結合子の違い
- 疑似クラスと疑似要素の役割の違い
- 詳細度の計算方法と、列ごとに比較するという規則
- 詳細度が同じときに何が勝敗を決めるか
ボックスモデルを読んでいると、コード例のプロパティが分かりやすくなります。必須ではありません。
セレクタは要素を選ぶ仕組み
セレクタは、スタイルを当てる対象の要素を指定する記述です。HTML のタグ名やクラス名などを組み合わせて、文書の中から特定の要素を絞り込みます。
| 種類 | 書き方 | 選ぶもの |
|---|---|---|
| 型セレクタ | p | すべての p 要素 |
| クラスセレクタ | .card | class="card" を持つ要素 |
| ID セレクタ | #header | id="header" を持つ要素 |
| 属性セレクタ | [type="radio"] | その属性を持つ要素 |
| 全称セレクタ | * | すべての要素 |
これらは組み合わせられます。p.note は「クラス note を持つ p 要素」を意味し、間に空白を入れない点が結合子と異なります。
結合子は要素どうしの関係を表す
結合子 (combinator) は、セレクタとセレクタの間に置いて、両者の位置関係を指定する記号です。ここで最も間違えやすいのが 子孫結合子と子結合子の違いです。
| 結合子 | 書き方 | 選ぶもの |
|---|---|---|
| 子孫結合子 | div p (半角スペース) | div の中にあるすべての階層の p |
| 子結合子 | div > p | div の直下の子である p だけ |
| 隣接兄弟結合子 | h2 + p | h2 の直後にある兄弟の p |
| 後続兄弟結合子 | h2 ~ p | h2 より後ろにある兄弟の p すべて |
次の HTML で違いを見ます。
<div>
<p>A ー div の直下</p>
<section>
<p>B ー section の中 (div から見ると孫)</p>
</section>
</div>
| セレクタ | 選ばれる要素 |
|---|---|
div p | A と B の両方 |
div > p | A だけ |
「すべての階層」を選ぶのは空白 (子孫結合子) であって、> ではありません。> はむしろ範囲を直下の 1 階層に狭める記号です。この 2 つは意味が逆に近いので、取り違えると意図しない要素にスタイルが漏れます。
深い階層まで効かせたくないコンポーネントでは、> を使って範囲を閉じ込めるのが有効です。
/* カード直下の見出しだけに効かせる。入れ子のカードには漏らさない */
.card > h3 {
margin-top: 0;
}
疑似クラスと疑似要素
疑似クラス
MDN は疑似クラスを次のように定義しています。
A pseudo-class is a keyword added to a selector that lets you select elements based on information that lies outside of the document tree, such as a specific state of the selected element(s).
日本語にすると「文書ツリーの外側にある情報 (選択対象の状態など) をもとに要素を選べるようにする、セレクタに付加するキーワード」です。コロン 1 つで書きます。
「文書ツリーの外側の情報」という言い方が要点です。HTML を見ただけでは分からない条件で要素を選べます。マウスが乗っているか、フォーカスが当たっているか、兄弟の中で何番目か。こうした情報はマークアップには書かれていません。
| 分類 | 例 | 何で選ぶか |
|---|---|---|
| 状態 | :hover / :focus / :active | ユーザーの操作 |
| フォーム状態 | :checked / :disabled / :valid | 入力要素の状態 |
| 構造・位置 | :first-child / :last-child / :nth-child() | 兄弟の中での位置 |
| 言語 | :lang() / :dir() | コンテンツの言語や書字方向 |
疑似クラスが選ぶのは、あくまで既に存在する要素です。状態や位置という条件を足して絞り込んでいるだけで、新しい要素を作るわけではありません。
疑似要素
疑似要素は、要素の一部分を選んだり、文書に存在しない部分を作り出したりします。コロン 2 つで書きます。
.note::before {
content: "注意: ";
font-weight: bold;
}
input::placeholder {
color: #999;
}
| 疑似クラス | 疑似要素 | |
|---|---|---|
| 記法 | コロン 1 つ (:hover) | コロン 2 つ (::before) |
| 選ぶもの | 要素そのもの | 要素の一部分、または生成した部分 |
| 代表例 | :hover / :first-child | ::before / ::after / ::placeholder |
コロンの数は歴史的な事情で混在しています。:before のようにコロン 1 つでも古いブラウザ互換のために動きますが、新しく書くなら疑似要素はコロン 2 つに揃えます。
詳細度
同じ要素の同じプロパティに複数の宣言が当たったとき、どれを採用するかを決める重みが 詳細度 (specificity) です。
詳細度は 3 つの列を持つ値として表します。MDN の記法にならって ID-CLASS-TYPE の順で書きます。
| 列 | 含まれるもの | 加算される値 |
|---|---|---|
| ID | ID セレクタ (#header) | 1-0-0 |
| CLASS | クラス (.card)、属性セレクタ ([type="radio"])、疑似クラス (:hover) | 0-1-0 |
| TYPE | 型セレクタ (p)、疑似要素 (::before) | 0-0-1 |
**重みの順序は ID → クラス → 型で、この向きに高くなります。**クラスと ID を逆に覚えていると、実際の勝敗と食い違います。
何も加算しないもの
- 全称セレクタ
*は0-0-0です。すべての要素にマッチしますが、詳細度には一切寄与しません - 結合子 (
>/+/~/ 半角スペース) も詳細度を上げません。選ぶ範囲は変わりますが、重みは変わりません :where()は中に何を書いても0-0-0になります
つまり全称セレクタは「詳細度の階段の一番下」ではなく、階段に乗っていないのです。
列ごとに比べる。桁上がりはしない
詳細度の比較は、左の列から順に見ます。上位の列で差がついたら、下位の列がどれだけ大きくても逆転しません。
#myElement {
color: green; /* 1-0-0 ← 勝つ */
}
.a .b .c [id="myElement"] {
color: yellow; /* 0-4-0 */
}
クラス相当が 4 つあっても、ID が 1 つあるほうが勝ちます。10 進数の足し算 (100 対 40) として覚えても同じ結果になる場面が多いのですが、それは偶然です。クラスを 11 個並べても ID 1 つには勝てないので、桁上がりしない列比較として理解してください。
引数の重みを引き継ぐ疑似クラス
:is() / :not() / :has() は、それ自体は重みを持ちません。代わりに、引数の中で最も重いセレクタの重みを採用します。
:is(p, #fakeId) { } /* 1-0-0 ー 引数の中で最も重い #fakeId を採用 */
:where(p, #fakeId) { } /* 0-0-0 ー 常にゼロ */
:where() は詳細度をゼロにするための道具です。ライブラリの既定スタイルを :where() で包んでおくと、利用側がクラス 1 つで上書きできるようになります。
インラインスタイルと !important
この 2 つは詳細度の 3 列の外側にあります。
インラインスタイル (style="color: red") は、通常のスタイルシートの宣言をすべて上回ります。詳細度でいえば 1-0-0-0 に相当すると考えれば分かりやすくなります。
!important は詳細度とは別の仕組みです。MDN の表現では「技術的には詳細度とは関係ない」が、カスケードを通じて詳細度と相互作用します。同じ出自・同じレイヤーの中では、!important が付いた宣言が付いていない宣言に勝ちます。両方に !important が付いている場合は、その中で詳細度の高いほうが勝ちます。
カスケード — 詳細度だけでは決まらない
詳細度は勝敗を決める要素のひとつにすぎません。カスケードは次の順で判定します。
- 関連性 — セレクタが実際にその要素にマッチするか、メディアクエリの条件を満たすか
- 出自と重要度 — ブラウザ既定・ユーザー設定・作者スタイルのどれか、
!importantが付いているか - 詳細度 — 出自が同じなら、ここで比べる
- スコープの近さ — 詳細度も同じなら、スコープ元により近いほうが勝つ
- 記述順 — すべて同じなら、後に書いたほうが勝つ
!important は出自の順序を反転させます。通常は作者スタイルがユーザー設定より優先されますが、!important が付くと逆転してユーザー設定のほうが強くなります。アクセシビリティのためにユーザーが上書きしたスタイルを、サイト側が握りつぶせないようにするための設計です。
実務で覚えておく価値があるのは 5 番目です。
.button { color: blue; }
.button { color: red; } /* こちらが勝つ */
詳細度が同じなら、後に書いたほうが勝ちます。CSS ファイルの読み込み順や、ビルド後のバンドル順で結果が変わるのはこのためです。「なぜかスタイルが効かない」ときは、詳細度を上げる前に記述順を疑うほうが健全です。
詳細度の競合そのものを設計で避ける道もあります。ユーティリティファーストの考え方では、単一クラスの小さなスタイルを組み合わせて書くため詳細度がほぼ一定に保たれ、勝敗が記述順だけで決まります。詳細度を上げ合う消耗を構造的に起こしにくくする発想です。
よくある誤解
「ID とクラスなら、書いた順や見た目の細かさで強さが決まる」 — 決まりません。3 つの数の組で比べ、クラスより ID のほうが常に強くなります。クラスをいくつ重ねても ID 1 つには届きません。
「全称セレクタ * は最も弱い」 — 弱いのではなく、詳細度にまったく寄与しません。値は 0-0-0 で、順位の一番下に並ぶのではなく計算に入らないという扱いです。
「div > p は div の中のすべての階層の p を選ぶ」 — それは子孫結合子 div p (半角スペース) の動作です。div > p は直下の子だけを選びます。
「詳細度は 10 進数で足し算できる」 — 列ごとの比較なので桁上がりしません。クラスをいくつ重ねても ID 1 つには勝てません。
「!important は詳細度が最強という意味」 — !important は詳細度の値ではなく、カスケードの「出自と重要度」の段階で効きます。同じ重要度どうしになって初めて詳細度が比べられます。
「疑似クラスと疑似要素はコロンの数が違うだけ」 — 役割が違います。疑似クラスは既存の要素を状態や位置で絞り込み、疑似要素は要素の一部分を選ぶか新しい部分を生成します。
確認問題
問 1. 次の CSS と HTML で、span の文字色は何になりますか。
/* A */
#main .title { color: blue; }
/* B */
.page .section .title span { color: red; }
<div id="main" class="page">
<h1 class="title"><span>見出し</span></h1>
</div>
答え: blue
B の詳細度は 0-3-1 で、A の 1-1-0 に対してクラス列では上回っています。しかし詳細度を比べる前に見るべきことがあります。
B は .page .section .title span なので、.section を持つ祖先要素が必要です。この HTML には存在しないため、B はそもそもマッチしません。カスケードの第 1 段階「関連性」で候補から外れ、詳細度の比較には進みません。
残った A は h1.title にマッチして青を与えます。span 自身を選ぶ宣言はありませんが、color は継承されるので span も青になります。
**詳細度を計算する前に、そのセレクタが本当にマッチしているかを確かめてください。**効かないスタイルの原因は、詳細度負けではなくセレクタの書き間違いであることが少なくありません。
問 2. 次の 3 つのセレクタの詳細度を求め、勝つものを答えてください。
A: #sidebar a:hover
B: .nav .item .link.active:hover
C: * body div ul li a
答え:
| ID | CLASS | TYPE | 表記 | |
|---|---|---|---|---|
| A | #sidebar = 1 | :hover = 1 | a = 1 | 1-1-1 |
| B | 0 | .nav .item .link .active :hover = 5 | 0 | 0-5-0 |
| C | 0 | 0 | body div ul li a = 5 | 0-0-5 |
勝つのは A です。ID 列で 1 対 0 対 0 となり、その時点で決着します。B のクラス 5 個も C の型 5 個も関係ありません。
C の先頭の * は詳細度に寄与しないので 0 個として数えます。ただし選ぶ範囲には影響します。* body は「何らかの要素の子孫である body」を意味し、body の祖先である html が * にマッチするので成立します。もし * html と書いていたら、html はルート要素で祖先を持たないため、このセレクタは何にもマッチしません。詳細度の計算とマッチ判定は別の話です。
問 3. :where(.a, .b, #c) p の詳細度はいくつですか。
答え: 0-0-1
:where() は中身に何を書いても常に 0-0-0 になります。したがって寄与するのは p の型セレクタだけで、0-0-1 です。
比較のために、同じ内容を :is() で書くと :is(.a, .b, #c) p は 1-0-1 になります。:is() は引数の中で最も重い #c の重みを採用するためです。
問 4. 次の CSS で、ボタンの背景色は何になりますか。
.btn { background: blue; }
.btn { background: green; }
button.btn { background: red; }
<button class="btn">送信</button>
答え: red
詳細度を比べます。
.btn(1 行目・2 行目) は0-1-0button.btn(3 行目) は0-1-1
TYPE 列で button のぶんだけ 3 行目が上回るので、記述順に関係なく red が勝ちます。
もし 3 行目が無ければ、1 行目と 2 行目は詳細度が同じ 0-1-0 なので、後に書いた green が勝ちます。
まとめ
- セレクタは型・クラス・ID・属性・全称の組み合わせで要素を選びます
- 子孫結合子 (半角スペース) はすべての階層を選び、子結合子
>は直下の子だけを選びます - 疑似クラスはコロン 1 つで、文書ツリーの外側の情報 (状態や位置) をもとに既存の要素を絞り込みます
- 疑似要素はコロン 2 つで、要素の一部分を選ぶか新しい部分を生成します
- 詳細度は
ID-CLASS-TYPEの 3 列で表し、左の列から順に比較します。桁上がりはしません - 全称セレクタと結合子は詳細度に寄与しません。
:where()も常にゼロです - 詳細度が同じなら後に書いたほうが勝ちます。効かないときは記述順を先に疑ってください
- Tailwind CSS ガイド — 状態バリアント — 疑似クラスをユーティリティクラスで扱う方法
- Tailwind CSS ガイド — ユーティリティファーストの考え方 — 詳細度の競合を設計で避ける発想
- MDN — 詳細度
- MDN — カスケード
- MDN — 疑似クラス
次に読む
- 宣言的 UI と命令的 UI — 状態から UI を導く書き方へ進みます