- CSS
CSSセレクタ一覧|:has()・ネスト対応の早見表と詳細度
CSSセレクタとは、「どの要素にスタイルを当てるか」をHTMLの中から選び出すための書き方です。 型・クラス・IDの基本から、要素どうしの関係を表す結合子、[href$=".pdf" i] のような属性の条件、:nth-child() や :focus-visible などの擬似クラス、::before などの擬似要素、:has() とネスト(&)まで押さえると、JavaScriptに頼らずに済む場面がぐっと増えます。
この記事では、すべてのセレクタを「構文 / 意味 / 例 / 詳細度 / 対応状況」の早見表 にまとめ、Playwright + Chromium 141 で「各セレクタが一致した要素数」「ルールが競合したときの getComputedStyle の値」「フォーカスやフォーム状態の変化」を実測 しました。対応状況は MDN の browser-compat-data 8.1.3 と web-features 3.40.0(2026年9月時点)で確認しています。よくあるつまずき(子孫と子、:nth-child と :nth-of-type、LVHA の順序、二重コロン)と、コピペで使える実務パターン集も載せています。
CSSセレクタ早見表(詳細度・対応状況つき)
詳細度は「ID の数 - クラス等の数 - 型の数」で表記します。対応状況は Chrome / Firefox / Safari の対応開始バージョンで、◎ は 2020 年以前から主要ブラウザすべてで使えるもの です。
基本セレクタと結合子
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
* |
すべての要素 | * { box-sizing: border-box } |
0-0-0 | ◎ |
E |
型(タグ名) | li { margin: 0 } |
0-0-1 | ◎ |
.class |
クラス | .item { padding: 8px } |
0-1-0 | ◎ |
#id |
ID | #name { width: 100% } |
1-0-0 | ◎ |
E.a.b |
複合(すべての条件を満たす) | li.item.current |
0-2-1 | ◎ |
A, B |
セレクタリスト(いずれか) | h2, h3 { color: navy } |
それぞれ個別 | ◎ |
A B |
子孫結合子(A の中のすべての B) | article p |
合算(0-0-2) | ◎ |
A > B |
子結合子(A の直下の B) | article > p |
合算 | ◎ |
A + B |
次兄弟結合子(A の直後の B) | h2 + p |
合算 | ◎ |
A ~ B |
後続兄弟結合子(A より後ろの兄弟 B) | h2 ~ p |
合算 | ◎ |
A || B |
列結合子(表の列に属するセル) | col.total || td |
合算 | 未対応(Chromium 141 で SyntaxError。Firefox / Safari も未実装) |
属性セレクタ
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
[attr] |
属性がある | [required] |
0-1-0 | ◎ |
[attr="v"] |
値が完全一致 | [type="checkbox"] |
0-1-0 | ◎ |
[attr~="v"] |
空白区切りの語のひとつが v | [class~="current"] |
0-1-0 | ◎ |
[attr|="v"] |
v または v- で始まる(言語コード向け) |
[lang|="en"] |
0-1-0 | ◎ |
[attr^="v"] |
v で始まる | [href^="https://"] |
0-1-0 | ◎ |
[attr$="v"] |
v で終わる | [href$=".pdf"] |
0-1-0 | ◎ |
[attr*="v"] |
v を含む | [href*="example"] |
0-1-0 | ◎ |
[attr="v" i] |
大文字小文字を区別しない | [href$=".pdf" i] |
0-1-0 | Chrome 49・Firefox 47・Safari 9 |
[attr="v" s] |
区別を強制する | [data-key="A" s] |
0-1-0 | Firefox 66・Safari 27。Chromium 未対応(SyntaxError) |
構造擬似クラス(位置・ツリー)
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
:root |
文書のルート(html) |
:root { --gap: 8px } |
0-1-0 | ◎ |
:first-child / :last-child |
最初 / 最後の子 | li:first-child |
0-1-0 | ◎ |
:nth-child(An+B) |
兄弟の中で An+B 番目(odd / even も可) |
tr:nth-child(even) |
0-1-0 | ◎ |
:nth-last-child(An+B) |
後ろから数えて An+B 番目 | li:nth-last-child(1) |
0-1-0 | ◎ |
:nth-child(An+B of S) |
S に当てはまる兄弟だけを数える | li:nth-child(2 of .done) |
0-1-0 + S の最高値 | Chrome 111・Firefox 113・Safari 9(Baseline 2023) |
:first-of-type / :last-of-type |
同じ型の中で最初 / 最後 | p:first-of-type |
0-1-0 | ◎ |
:nth-of-type(An+B) / :nth-last-of-type() |
同じ型の中で An+B 番目 | p:nth-of-type(2) |
0-1-0 | ◎ |
:only-child / :only-of-type |
唯一の子 / 同じ型で唯一 | .box p:only-child |
0-1-0 | ◎ |
:empty |
子要素もテキストもない | .note:empty { display: none } |
0-1-0 | ◎ |
状態擬似クラス(ユーザー操作・リンク)
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
:hover |
ポインタが乗っている(祖先にも当たる) | a:hover |
0-1-0 | ◎ |
:active |
押している最中 | button:active |
0-1-0 | ◎ |
:focus |
フォーカスがある | input:focus |
0-1-0 | ◎ |
:focus-visible |
フォーカスリングを表示すべきとき(キーボード操作など) | button:focus-visible |
0-1-0 | Chrome 86・Firefox 85・Safari 15.4(Baseline 2022) |
:focus-within |
自分か子孫にフォーカスがある | form:focus-within |
0-1-0 | Chrome 60・Firefox 52・Safari 10.1 |
:link / :visited |
未訪問 / 訪問済みリンク | a:link |
0-1-0 | ◎(:visited は色系のみ変更可) |
:target |
URL の #id に一致する要素 |
section:target |
0-1-0 | ◎ |
入力・フォームの擬似クラス
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
:checked |
チェック / 選択されている | input:checked + label |
0-1-0 | ◎ |
:indeterminate |
未確定(JS で設定したチェックボックス、選択なしのラジオ群、値なしの progress) | input:indeterminate |
0-1-0 | ◎ |
:disabled / :enabled |
無効 / 有効 | button:disabled |
0-1-0 | ◎ |
:required / :optional |
必須 / 任意 | input:required |
0-1-0 | ◎ |
:valid / :invalid |
制約検証に合格 / 不合格(読み込み直後から判定) | input:invalid |
0-1-0 | ◎ |
:user-valid / :user-invalid |
ユーザーが操作した後だけ判定 | input:user-invalid |
0-1-0 | Chrome 119・Firefox 88・Safari 16.5(Baseline 2023) |
:placeholder-shown |
プレースホルダが表示中(未入力) | input:placeholder-shown |
0-1-0 | Chrome 47・Firefox 51・Safari 9 |
:in-range / :out-of-range |
min / max の範囲内 / 外 | input:out-of-range |
0-1-0 | ◎ |
:read-only / :read-write |
編集不可 / 可(:read-only は入力欄以外にも一致) |
input:read-only |
0-1-0 | ◎ |
論理擬似クラス
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
:is(A, B) |
いずれかに一致(無効なセレクタは無視される) | :is(h1, h2, h3) a |
引数の中で最も高いもの | Chrome 88・Firefox 78・Safari 14 |
:where(A, B) |
:is() と同じ一致で、詳細度が常に 0 |
:where(ul, ol) { padding: 0 } |
0-0-0 | Chrome 88・Firefox 78・Safari 14 |
:not(A, B) |
いずれにも一致しない | li:not(.done, .hidden) |
引数の中で最も高いもの | ◎(複数引数は Chrome 88・Firefox 84・Safari 9) |
:has(A) |
A に当てはまる要素を中(または後ろ)に持つ | label:has(+ input:required) |
引数の中で最も高いもの | Chrome 105・Firefox 121・Safari 15.4(2026年6月に Baseline Widely available) |
:has() は別記事「CSS :has() 使い方」で15パターンを検証しているため、この記事では表と実務パターンの1例に留めます。
擬似要素
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
::before / ::after |
要素の先頭 / 末尾に生成する内容(content 必須) |
.req::after { content: " *" } |
0-0-1 | ◎(単一コロン :before も互換のため可) |
::first-line / ::first-letter |
最初の行 / 最初の文字 | p::first-letter { font-size: 2em } |
0-0-1 | ◎ |
::placeholder |
プレースホルダ文字 | input::placeholder { color: gray } |
0-0-1 | Chrome 57・Firefox 51・Safari 10.1 |
::marker |
リストの行頭記号 | li::marker { color: navy } |
0-0-1 | Chrome 86・Firefox 68・Safari 11.1(Safari は color と font-size のみ) |
::selection |
選択範囲 | ::selection { background: yellow } |
0-0-1 | Chrome 1・Firefox 62・Safari 1.1(iOS Safari は未対応) |
::backdrop |
モーダルダイアログや全画面要素の背面 | dialog::backdrop |
0-0-1 | Chrome 37・Firefox 47・Safari 15.4 |
::file-selector-button |
input type="file" のボタン |
input::file-selector-button |
0-0-1 | Chrome 89・Firefox 82・Safari 14.1 |
ネスト(&)
| 書き方 | 展開されるセレクタ | 詳細度 | 対応状況 |
|---|---|---|---|
.card { .title {} } |
.card .title(& 省略時は子孫) |
0-2-0 | Chrome 120・Firefox 117・Safari 17.2(2026年6月に Baseline Widely available) |
.card { &.active {} } |
.card.active(複合) |
0-2-0 | 同上 |
.card { .dark & {} } |
.dark .card(& を後ろに置く) |
0-2-0 | 同上 |
.card { & + & {} } |
.card + .card |
0-2-0 | 同上 |
.card { p {} } |
.card p(型セレクタ始まりも可) |
0-1-1 | Chrome 120・Safari 17.2 以降(それ以前は & p が必要) |
.card { @media (...) {} } |
@media の中に .card |
0-1-0 | 同上 |
#a, .b { & p {} } |
:is(#a, .b) p と同じ扱い |
1-0-1(リストの最高値) | 同上 |
その他の擬似クラス
| 構文 | 意味 | 例 | 詳細度 | 対応状況 |
|---|---|---|---|---|
:lang(ja) |
言語(lang 属性の継承を含む) |
:lang(en) q { quotes: '"' '"' } |
0-1-0 | ◎(:lang(en, fr) の複数指定は Firefox 114・Safari 9。Chromium 未対応) |
:dir(rtl) |
文字方向(dir 属性や継承で決まる意味上の方向) |
:dir(rtl) .icon { transform: scaleX(-1) } |
0-1-0 | Chrome 120・Firefox 49・Safari 16.4(Baseline 2023) |
:defined |
定義済みの要素(未定義のカスタム要素は非一致) | my-widget:not(:defined) { visibility: hidden } |
0-1-0 | Chrome 54・Firefox 63・Safari 10 |
:modal |
showModal() で開いたダイアログ |
dialog:modal { border: 0 } |
0-1-0 | Chrome 105・Firefox 103・Safari 15.6 |
:popover-open |
表示中のポップオーバー | [popover]:popover-open |
0-1-0 | Chrome 114・Firefox 125・Safari 17 |
:open |
開いている dialog / details / select など |
details:open |
0-1-0 | Chrome 133・Firefox 136・Safari 26.5(Baseline 2026) |
:state(name) |
カスタム要素が ElementInternals.states に追加した状態 |
my-widget:state(loaded) |
0-1-0 | Chrome 125・Firefox 126・Safari 17.4(Baseline 2024) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSセレクタ一覧|:has()・ネスト対応の早見表と詳細度」
検証環境と、一致数を数えるサンプルHTML
| 項目 | 内容 |
|---|---|
| ブラウザ | Chromium 141.0.7390.37(Playwright 1.56.1、Node.js 22.22) |
| 確認方法 | document.querySelectorAll(セレクタ).length で一致数、CSS.supports("selector(...)") で構文対応、getComputedStyle() で適用結果 |
| 対応状況の出典 | @mdn/browser-compat-data 8.1.3、web-features 3.40.0 |
次のHTMLを index.html として保存すると、下の表の数字をそのまま再現できます。
<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>CSS selector test</title></head>
<body>
<nav id="menu">
<ul class="nav">
<li class="item"><a href="#top">トップ</a></li>
<li class="item current"><a href="/orders" data-count="3">注文一覧</a></li>
<li class="item"><a href="https://example.com/guide.PDF" target="_blank">資料 (PDF)</a></li>
<li class="item"><a href="https://example.com/help">ヘルプ</a></li>
</ul>
</nav>
<main>
<article class="post" lang="en">
<h2 id="title">Selector test page</h2>
<p class="lead">Lead paragraph</p>
<p>Second paragraph with <span>span</span> and <strong>strong</strong></p>
<ul class="list">
<li>A</li><li class="important">B</li><li>C</li><li class="important">D</li><li>E</li>
</ul>
<p></p>
<div class="box"><p>nested p in div</p></div>
</article>
<form id="order-form" novalidate>
<label for="name">お名前</label>
<input id="name" name="name" type="text" required placeholder="山田 太郎">
<input id="qty" name="qty" type="number" min="1" max="10" value="3">
<input id="qty2" name="qty2" type="number" min="1" max="10" value="20">
<input id="agree" type="checkbox">
<input id="all" type="checkbox">
<input id="r1" type="radio" name="size" value="s" checked>
<input id="r2" type="radio" name="size" value="m">
<input id="memo" type="text" disabled value="x">
<input id="ro" type="text" readonly value="y">
<input id="email" type="email" value="not-an-email">
<input id="file" type="file">
<button id="submit" type="submit">送信</button>
<button id="btn" type="button">下書き保存</button>
</form>
<script>document.getElementById("order-form").addEventListener("submit", e => e.preventDefault());</script>
<table class="orders">
<thead><tr><th>ID</th><th>品名</th></tr></thead>
<tbody><tr><td>1</td><td>備品A</td></tr><tr><td>2</td><td>備品B</td></tr><tr><td>3</td><td>備品C</td></tr><tr><td>4</td><td>備品D</td></tr></tbody>
</table>
<p id="rtl" dir="rtl">مرحبا</p>
<my-widget id="w1"></my-widget>
<dialog id="dlg"><p>dialog</p></dialog>
<div id="pop" popover>popover</div>
<section id="top"><h3>top</h3></section>
</main>
</body>
</html>
一致数は Playwright で開いたページ上で document.querySelectorAll(sel).length を実行して数えました(npm i playwright@1.56 → const { chromium } = require('playwright') → page.goto('file://.../index.html') → page.evaluate(...))。構文エラーになるセレクタは try { ... } catch (e) { e.name + ': ' + e.message } で捕まえています。
基本・結合子・属性・構造擬似クラスの一致数(Chromium 141)
| セレクタ | 意味 | 一致数 |
|---|---|---|
h2, h3 |
セレクタリスト | 2(CSS.supports("selector(h2, h3)") は false。selector() には単一のセレクタしか渡せない) |
article p |
子孫(.box の中の p も含む) |
4 |
article > p |
子(直下だけ) | 3 |
thead || td |
列結合子 | エラー(下のエラー文) |
[class~="current"] / [lang|="en"] |
空白区切りの1語 / en または en- 始まり |
1 / 1 |
[href$=".pdf"] |
後方一致(guide.PDF は大文字なので不一致) |
0 |
[href$=".pdf" i] |
大文字小文字を区別しない | 1 |
[href$=".pdf" s] |
区別を強制 | エラー(SyntaxError) |
.list li:nth-child(2 of .important) |
.important の中で2番目(D) |
1 |
.list li.important:nth-child(2) |
兄弟全体で2番目かつ .important(B) |
1 |
article p:nth-of-type(2) |
p の中で2番目 | 1 |
article p:nth-child(2) |
2番目の子(h2 の次)がたまたま p なので一致 | 1 |
article p:first-of-type |
article 直下と .box の中、それぞれの最初の p |
2 |
列結合子のエラー文はそのまま次のとおりでした。
SyntaxError: Failed to execute 'querySelectorAll' on 'Document': 'thead || td' is not a valid selector.
状態・入力・その他の擬似クラス(読み込み直後)
| セレクタ | 一致数 | 補足 |
|---|---|---|
a:link / a:visited |
4 / 0 | querySelectorAll(':visited') はプライバシー保護のため常に 0 |
:valid / :invalid |
8 / 4 | :invalid は name・qty2・email と form 自身 |
input:user-invalid |
0 | 操作前は一致しない(次の表) |
:read-only / :read-write |
68 / 4 | :read-only は編集できない要素すべて(div や p も)に一致 |
p::before |
0 | 擬似要素は querySelectorAll では取れない(エラーにはならない) |
:lang(en) / :dir(rtl) |
15 / 1 | article lang="en" の子孫が 15 |
操作で変わる擬似クラスの実測(操作前 → 操作後)
:hover や :focus-visible は「いつ一致するか」が分かりにくいので、Playwright で操作して一致要素の id を記録しました。
| セレクタ | 操作 | 操作前 | 操作後 |
|---|---|---|---|
li:hover |
page.hover('li.current') |
なし | li |
:focus |
マウスでボタン #btn をクリック |
なし | btn |
:focus-visible |
マウスでボタン #btn をクリック |
なし | なし(ボタンのクリックではリングを出さない) |
:focus-visible |
マウスでテキスト入力 #name をクリック |
なし | name(入力欄は常に表示) |
:focus-within |
同上 | なし | html, body, main, order-form, name |
:focus-visible |
#btn クリック後、Shift+Tab で #submit へ |
なし | submit(キーボード移動では表示) |
:target |
URL を index.html#top に変更 |
なし | top |
input:indeterminate |
all.indeterminate = true |
なし | all |
:user-invalid |
#name に "a" を入力して消し、フォーカスを外す |
なし | name |
:user-invalid |
さらに送信ボタンを押す(送信を試みる) | name, qty2, email(全欄が対象になる) |
|
:user-valid |
#qty を 5 にしてフォーカスを外す |
なし | qty |
:modal |
dlg.showModal() |
なし | dlg(dialog:open にも一致) |
:modal |
dlg.show()(非モーダル) |
なし(dialog[open] には一致) |
|
:popover-open |
pop.showPopover() |
なし | pop |
my-widget:state(loaded) |
定義内で this.attachInternals().states.add('loaded') |
なし | w1 |
:invalid は読み込み直後から一致するのに対し、:user-invalid は「ユーザーが触った後」または「送信を試みた後」だけ一致します。 入力前からエラー表示になるのを避けたいフォームでは :user-invalid を選ぶとよいです。
詳細度の計算表と、競合したときにどれが勝つか
詳細度(specificity)は「ID の数、クラス・属性・擬似クラスの数、型・擬似要素の数」の3桁で比べ、左の桁から大きいほうが勝ちます。 同じ詳細度なら後に書いたルールが勝ちます。
| セレクタ | ID | クラス等 | 型等 | 表記 |
|---|---|---|---|---|
* |
0 | 0 | 0 | 0-0-0 |
p / p::before |
0 | 0 | 1 / 2 | 0-0-1 / 0-0-2 |
.a / [type="text"] / :hover |
0 | 1 | 0 | 0-1-0 |
p.a |
0 | 1 | 1 | 0-1-1 |
.a.b / .a.a(同じクラスの繰り返しも数える) |
0 | 2 | 0 | 0-2-0 |
#name |
1 | 0 | 0 | 1-0-0 |
nav ul li a |
0 | 0 | 4 | 0-0-4 |
:is(#a, .b) / :not(#a) / :has(#a) |
1 | 0 | 0 | 1-0-0(引数の最高値) |
:where(#a, .b) |
0 | 0 | 0 | 0-0-0 |
li:nth-child(2 of .done) |
0 | 2 | 1 | 0-2-1(0-1-0 + S の最高値) |
#a, article { & p {} } の & p |
1 | 0 | 1 | 1-0-1 |
style="..."(インライン) |
— | — | — | すべてのセレクタより優先 |
!important |
— | — | — | インラインよりさらに優先(!important 同士は詳細度で比較) |
Chromium 141 で実測した勝敗
次のCSSとHTMLで、どのルールの色が getComputedStyle().color に残るか を確認しました。
/* 1: 型 < クラス < ID(書いた順に関係なく) */
#t1 { color: blue; } .a { color: green; } p { color: red; }
/* 2: :is() は引数の中で最も高い詳細度(#t2 → 1-0-0) */
:is(#t2, .x) { color: blue; } .x.x { color: red; } p.x.x.x { color: red; }
/* 3: :where() は 0-0-0 */
:where(#t3, .y) { color: blue; } p.t3 { color: red; }
/* 4: インライン > ID(HTML 側は style="color: red") */
#t4 { color: blue; }
/* 5: !important > インライン */
p.t5 { color: blue !important; }
/* 6: ネストの & は親リストの最高値を取る(#nothing は存在しない ID) */
#nothing, article { & p { color: blue; } } /* 1-0-1 */
.post p { color: red; } /* 0-1-1 */
/* 7: :not() の中身も数える */
.w8 p:not(#never) { color: blue; } /* 1-1-1 */ #t8 { color: red; } /* 1-0-0 */
/* 8: 同じ詳細度なら後勝ち */
.a10 { color: blue; } .b10 { color: red; }
/* 9: @layer の中は層の外に負ける */
@layer base { #t11 { color: red; } } .a11 { color: blue; }
/* 10: LVHA の順序 */
a.lv1:hover { color: red; } a.lv1:link { color: blue; }
a.lv2:link { color: blue; } a.lv2:hover { color: red; }
<p id="t1" class="a">t1</p> <p id="t2" class="x">t2</p> <p id="t2b" class="x">t2b</p>
<p id="t3" class="y t3">t3</p> <p id="t4" style="color: red;">t4</p>
<p id="t5" class="t5" style="color: red;">t5</p>
<article class="post"><p id="t7">t7</p></article> <div class="w8"><p id="t8">t8</p></div>
<p id="t10" class="b10 a10">t10</p> <p id="t11" class="a11">t11</p>
<a class="lv1" href="https://example.com/1">lv1</a> <a class="lv2" href="https://example.com/2">lv2</a>
| # | 要素 | 競合したルール | 勝ったルール | getComputedStyle().color |
|---|---|---|---|---|
| 1 | t1 | #t1 1-0-0 / .a 0-1-0 / p 0-0-1 |
#t1 |
rgb(0, 0, 255)(blue) |
| 2 | t2b | .x で一致しただけでも :is(#t2, .x) は 1-0-0 / p.x.x.x 0-3-1(後) |
:is() |
rgb(0, 0, 255)(blue) |
| 3 | t3 | :where(#t3, .y) 0-0-0 / p.t3 0-1-1 |
p.t3 |
rgb(255, 0, 0)(red) |
| 4 | t4 | #t4 1-0-0 / インライン |
インライン | rgb(255, 0, 0)(red) |
| 5 | t5 | インライン / p.t5 !important |
!important |
rgb(0, 0, 255)(blue) |
| 6 | t7 | ネスト & p 1-0-1 / .post p 0-1-1(後) |
ネスト側 | rgb(0, 0, 255)(blue) |
| 7 | t8 | .w8 p:not(#never) 1-1-1 / #t8 1-0-0(後) |
:not() 側 |
rgb(0, 0, 255)(blue) |
| 8 | t10 | .a10 / .b10(同じ 0-1-0、.b10 が後) |
後に書いた .b10 |
rgb(255, 0, 0)(red) |
| 9 | t11 | @layer base { #t11 } / 層の外の .a11 |
層の外 | rgb(0, 0, 255)(blue) |
| 10 | lv1 | :hover を先、:link を後に書いた |
hover 中も :link |
通常 blue → hover 中も rgb(0, 0, 255) |
| 10 | lv2 | :link を先、:hover を後に書いた |
hover 中は :hover |
通常 blue → hover 中 rgb(255, 0, 0) |
特に注意したいのが t2b です。:is(#t2, .x) は .x 側で一致した要素にも「引数の中で最も高い 1-0-0」が付くため、後から書いた p.x.x.x(0-3-1)でも上書きできません。 詳細度を上げたくないグループ化には :where() を使います。
ネスト(&)の書き方と展開のされ方
CSS ネストは、Sass のように親セレクタの中に子のルールを書ける標準機能です(Chrome 120・Firefox 117・Safari 17.2 以降)。 & は「親セレクタそのもの」を表し、省略すると子孫結合子として扱われます。
.card {
color: rgb(10, 10, 10);
.title { color: rgb(1, 1, 1); } /* → .card .title(& 省略 = 子孫) */
&.active { color: rgb(2, 2, 2); } /* → .card.active(& と密着させる) */
.dark & { color: rgb(3, 3, 3); } /* → .dark .card(& を後ろに置ける) */
& + & { color: rgb(4, 4, 4); } /* → .card + .card */
p { color: rgb(5, 5, 5); } /* → .card p(型セレクタ始まりも可) */
&:hover { color: rgb(6, 6, 6); } /* → .card:hover */
@media (min-width: 1px) { color: rgb(7, 7, 7); } /* @media もネストできる */
}
Chromium 141 の CSSOM が保持していたネスト内の selectorText は ["& .title", "&.active", ".dark &", "& + &", "& p", "&:hover", "@media (min-width: 1px)"] で、& を省略した .title と p には自動で & が補われている ことが分かります。
| 要素 | 状況 | color |
|---|---|---|
1つ目の .card |
&.active でも & + & でもない |
rgb(7, 7, 7)(@media 内の宣言が後にあるので勝つ) |
2つ目の .card.active |
&.active(0-2-0)と & + &(0-2-0)の両方に一致 |
rgb(4, 4, 4)(同じ詳細度なので後に書いた & + &) |
.dark の中の .card / .title / p |
.dark & / & .title / & p |
rgb(3, 3, 3) / rgb(1, 1, 1) / rgb(5, 5, 5) |
1つ目の .card を hover |
&:hover |
rgb(6, 6, 6) |
ネストの詳細度は :is() と同じ規則で、親がセレクタリストなら「その中で最も高いもの」になります(前の表の t7 で実測:#nothing, article { & p } は 1-0-1)。意図せず詳細度が上がるのを避けたいときは、親側を :where() で包むか、ID をリストに混ぜないようにします。
よくあるつまずきと直し方
| つまずき | 起きること | 直し方 |
|---|---|---|
子孫 A B と子 A > B を混同 |
article p は .box の中の p も拾う(実測 4 件、article > p は 3 件) |
直下だけなら > を使う |
:nth-child() と :nth-of-type() を混同 |
p:nth-child(2) は「2番目の子が p のとき」だけ一致。間に h2 や div が挟まると当たらない |
同じ型で数えるなら :nth-of-type()、条件つきなら :nth-child(n of S) |
a:hover を a:link より先に書く |
同じ詳細度なので後の :link が勝ち、hover しても色が変わらない(実測 lv1) |
L-V-H-A の順(:link → :visited → :hover → :active)で書く |
| 擬似要素を単一コロンで書く | :before / :after は互換のため動くが、input:placeholder や li:marker は SyntaxError |
擬似要素は常に 二重コロン ::、擬似クラスは単一コロン |
:is() で詳細度が上がる |
:is(#id, .x) は .x で一致しても 1-0-0(実測 t2b) |
詳細度を足したくないときは :where() |
:not() に無効なセレクタを混ぜる |
:not(.a, :nope) はルールごと無効(SyntaxError)。:is() / :where() は無効な引数だけ無視する |
対応が不確かなセレクタは :is() に入れるか @supports selector() で分岐 |
:has() の入れ子・擬似要素 |
:has(:has(a)) と :has(::before) は SyntaxError |
:has() は1段だけ。擬似要素は外側に付ける |
| 擬似要素の後ろに擬似クラス | .a::before:hover は SyntaxError |
.a:hover::before の順に書く |
||(列結合子)や [attr="v" s] を使う |
Chromium 141 では SyntaxError |
列は td:nth-child(n) で指定し、大文字小文字は既定(区別あり)を使う |
:visited で幅や content を変える |
プライバシー保護のため色系以外は反映されず、querySelectorAll(':visited') も 0 件 |
訪問済みは色の変更だけに留める |
実務パターン集(コピペ用・Chromium 141 で適用値を確認)
/* P1 フォームのエラー表示(ユーザーが触った後だけ) */
.field input:user-invalid { border-color: rgb(220, 38, 38); }
.field .error { display: none; }
.field input:user-invalid + .error { display: block; }
/* P2 テーブルの縞 */
tbody tr:nth-child(even) { background-color: rgb(243, 244, 246); }
/* P3 最後の要素だけ余白なし */
.stack > * { margin-bottom: 16px; }
.stack > :last-child { margin-bottom: 0; }
/* P4 外部リンクに印(自サイトのドメインは除外) */
a[href^="http"]:not([href*="example.com"])::after { content: " ↗"; }
/* P5 必須項目のラベルに印(:has の1例) */
label:has(+ input:required)::after { content: " *"; color: rgb(220, 38, 38); }
/* P6 キーボード操作のときだけフォーカスリング */
button:focus-visible { outline: 3px solid rgb(37, 99, 235); }
button:focus:not(:focus-visible) { outline: none; }
/* P7 チェックで隣のラベルを強調 */
input[type="checkbox"]:checked + label { font-weight: 700; }
/* P8 先頭3つだけ強調 / 完了タスクの中で2番目だけ */
.list > li:nth-child(-n + 3) { font-weight: 700; }
.tasks > li:nth-child(2 of .done) { text-decoration-line: line-through; }
/* P9 モーダルの背面・行頭記号 */
dialog::backdrop { background-color: rgba(0, 0, 0, 0.5); }
li::marker { color: rgb(37, 99, 235); }
| # | パターン | 確認した getComputedStyle の値 |
|---|---|---|
| P1 | エラー表示 | 操作前:border-color: rgb(118, 118, 118) / .error は display: none → 入力して消しフォーカスを外した後:rgb(220, 38, 38) / block |
| P2 | テーブルの縞 | 1〜4行目の background-color:rgba(0, 0, 0, 0) / rgb(243, 244, 246) / rgba(0, 0, 0, 0) / rgb(243, 244, 246) |
| P3 | 最後だけ余白なし | margin-bottom:16px / 16px / 0px |
| P4 | 外部リンクの印 | 外部:content: " ↗" / 自サイト:none |
| P4 | 必須ラベルの印 | label::after の content: " *" |
| P5 | フォーカスリング | マウスクリック後:outline-style: none → Tab で戻った後:solid 3px rgb(37, 99, 235) |
| P6 | チェックで強調 | font-weight:400 → 700 |
| P7 | 先頭3つ / .done の2番目 |
font-weight:700 / 700 / 700 / 400。text-decoration-line:4件中 4番目(.done の2つ目)だけ line-through |
| P9 | 背面・行頭記号 | li::marker の color: rgb(37, 99, 235)(::backdrop は dialog 記事で検証) |
どれを選ぶかの判断表
| 迷う場面 | 選ぶもの | 理由 |
|---|---|---|
| グループ化したい | :is() |
短く書ける。詳細度は引数の最高値になる点だけ意識する |
| リセットCSS・上書きされやすい基礎スタイル | :where() |
詳細度 0 なので、あとから素直に上書きできる |
| フォームのエラー表示 | :user-invalid |
触る前から赤くならない。:invalid は読み込み直後から一致する |
| フォーカスの見た目 | :focus-visible |
マウスクリックではリングを出さず、キーボード操作では出す(実測どおり) |
| 「n 番目」を数えたい | 兄弟全体なら :nth-child()、同じ型なら :nth-of-type()、条件つきなら :nth-child(n of S) |
数える母集団が違う |
| 親や前の要素の見た目を変えたい | :has() |
2026年6月から Baseline Widely available。詳細度は引数の最高値 |
| 詳細度の競合を減らしたい | クラス中心にし、ID セレクタと !important は最小限に |
上書きの連鎖を防げる |
よくある質問
Q. :is() と :where() はどちらを使えばよいですか?
A. 一致する要素は同じで、違いは詳細度だけです。通常は :is() で問題ありませんが、リセットCSSやライブラリのように「あとで上書きされてほしい」スタイルは :where() にすると、利用側が詳細度を上げずに済みます。
Q. :nth-child(2) が当たらないのはなぜですか?
A. :nth-child() は「同じ親の子要素すべて」の中で数えます。h2 の次に p があると p:nth-child(2) は一致しますが、その前に div などが挟まると当たりません。同じ型の中で数えるなら :nth-of-type(2)、特定のクラスだけ数えるなら :nth-child(2 of .done) を使います。
Q. ネストはもう本番で使えますか? A. Chrome 120・Firefox 117・Safari 17.2 以降で対応し、2026年6月に Baseline Widely available になりました。それより前の環境も対象にする場合は、Sass や PostCSS でネストを展開して配信すると安心です。
まとめ
- セレクタは「基本・結合子・属性・擬似クラス・擬似要素・ネスト」の6グループ で整理すると覚えやすく、それぞれの詳細度と対応状況を早見表にまとめました。
- 子孫
A Bは 4 件、子A > Bは 3 件 のように、同じ HTML でも結合子で一致数が変わることを Chromium 141 で確認しました。 :focus-visibleはマウスクリックしたボタンでは一致せず、キーボード移動やテキスト入力欄では一致 します。:user-invalidは操作後または送信を試みた後だけ一致 し、:invalidは読み込み直後から一致します。:is()とネストの&は「引数(親リスト)の中で最も高い詳細度」 が付き、:where()は常に 0-0-0 です。- 詳細度は インライン > ID > クラス・属性・擬似クラス > 型・擬似要素 で、
!importantはさらに上、@layerの中は層の外に負けます。 ||と[attr="v" s]は Chromium 141 でSyntaxError、:visitedはquerySelectorAllで常に 0 件です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSセレクタ一覧|:has()・ネスト対応の早見表と詳細度」
株式会社RJCでは、CSSセレクタの一致数や詳細度の勝敗を実際に動かして確かめ、その結果をプロジェクトの中で仲間と共有しながら日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。