CSSの疑似クラス(擬似クラス)とは、a:hover や li:nth-child(2) のようにセレクタの後ろへコロン1つで付け、要素の「状態」や「位置」を条件にスタイルを当てるための書き方です。 HTMLに class を書き足さなくても、「マウスが乗っている間」「3番目の項目」「入力内容が正しくない欄」といった条件で要素を選べます。

この記事では、疑似クラスを状態・構造・フォーム・関数型の4つに分類した早見表(用途・コード例・詳細度・主要ブラウザの対応バージョン)を用意しました。さらに :nth-child() の式の対応表、:focus と :focus-visible の違い、:has() による親要素の選択、:where() の詳細度0、:nth-child と :nth-of-type の取り違え、スマートフォンで :hover が残る現象を、Chromium 141(Playwright 1.56)で実際に動かした結果とともに解説します。

疑似クラスの早見表

まずは出番の多い12個です。分類ごとの詳しい一覧は、このあとの章にあります。

疑似クラス 分類 いつ一致するか コード例 詳細度
:hover 状態 ポインターが要素の上にある間 a:hover { color: #c00; } 0,1,0
:active 状態 押している間 button:active { scale: 0.98; } 0,1,0
:focus 状態 フォーカスを受けている間 input:focus { border-color: #06c; } 0,1,0
:focus-visible 状態 フォーカスの目印が必要だとブラウザが判断したとき button:focus-visible { outline: 3px solid #06c; } 0,1,0
:first-child / :last-child 構造 兄弟の中で最初/最後 li:last-child { border: 0; } 0,1,0
:nth-child(式) 構造 兄弟の中で式に合う順番 tr:nth-child(even) { background: #f6f6f6; } 0,1,0
:nth-of-type(式) 構造 同じ種類のタグの中で式に合う順番 p:nth-of-type(1) { font-weight: 700; } 0,1,0
:checked フォーム チェックされている input:checked + span { font-weight: 700; } 0,1,0
:disabled フォーム 操作できない状態 input:disabled { opacity: .4; } 0,1,0
:not(セレクタ) 関数型 引数に一致しない li:not(.done) { color: #333; } 引数でいちばん高いもの
:is(セレクタ) / :where(セレクタ) 関数型 引数のどれかに一致 :is(h1, h2) a { color: inherit; } :is は引数でいちばん高いもの、:where は 0
:has(セレクタ) 関数型 引数に一致する子孫や次の兄弟を持つ .card:has(img) { padding: 0; } 引数でいちばん高いもの

詳細度(セレクタの強さ)は、疑似クラス1つにつき class 1つと同じ「0,1,0」が基本です。:is()・:not()・:has() は引数の中身で決まり、:where() だけは常に 0 になります。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CSS疑似クラス一覧|hover・nth-child・focus・notの使い方」

疑似クラスの基本と疑似要素との違い

疑似クラスは「コロン1つ」、疑似要素は「コロン2つ」で書きます。

種類 書き方 選ぶもの 例 詳細度
疑似クラス : 1つ すでにある要素を、状態や位置で絞り込む a:hover、li:first-child 0,1,0(class と同じ)
疑似要素 :: 2つ 要素の一部分や、HTMLに書いていない部分 p::before、p::first-line 0,0,1(タグと同じ)

疑似クラスはほかのセレクタと自由に組み合わせられます。

/* タグ + 疑似クラス */
a:hover { text-decoration: underline; }

/* class + 疑似クラス */
.btn:hover { background: #0057b8; }

/* 疑似クラスを続けて書く(両方に一致する要素) */
li:first-child:hover { background: #eef; }

/* 疑似クラス + 疑似要素 */
a:hover::after { content: " →"; }

なお「疑似クラス」「擬似クラス」はどちらの表記も使われており、指しているものは同じです。

状態の疑似クラス一覧(hover・active・focusなど)

ユーザーの操作やページの状態によって一致するものです。対応バージョンは npm の @mdn/browser-compat-data 8.1.4 のデータから拾っています(C=Chrome、E=Edge、F=Firefox、S=Safari)。

疑似クラス 一致する条件 コード例 対応バージョン
:hover ポインターが乗っている .card:hover { box-shadow: 0 4px 12px #0003; } C1 / E12 / F1 / S2
:active 押している最中 .btn:active { translate: 0 1px; } C1 / E12 / F1 / S1
:focus フォーカスがある input:focus { border-color: #06c; } C1 / E12 / F1 / S1
:focus-visible フォーカスの目印を出すべき場面 a:focus-visible { outline: 3px solid #06c; } C86 / E86 / F85 / S15.4
:focus-within 自分か子孫にフォーカスがある .field:focus-within { background: #f5faff; } C60 / E79 / F52 / S10.1
:link まだ訪れていないリンク a:link { color: #06c; } C1 / E12 / F1 / S1
:visited 訪問済みのリンク a:visited { color: #639; } C1 / E12 / F1 / S1
:any-link href を持つリンク全般 :any-link { text-underline-offset: .2em; } C65 / E79 / F50 / S9
:target URL の #id と一致する要素 section:target { scroll-margin-top: 80px; } C1 / E12 / F1 / S1.3
:popover-open 開いている popover [popover]:popover-open { opacity: 1; } C114 / E114 / F125 / S17
:modal モーダル表示中の dialog など dialog:modal { max-width: 40rem; } C105 / E105 / F103 / S15.6

リンクの4つの状態は L→V→H→A の順に書く

:link・:visited・:hover・:active は詳細度が同じなので、あとに書いたものが優先されます。:hover を :link より先に書くと、マウスを乗せても色が変わりません。

/* 直す前::hover が :link に上書きされる */
a.bad:hover { color: rgb(90, 0, 0); }
a.bad:link  { color: rgb(91, 0, 0); }

/* 直したあと:link → visited → hover → active の順 */
a.good:link    { color: rgb(91, 0, 0); }
a.good:visited { color: #639; }
a.good:hover   { color: rgb(90, 0, 0); }
a.good:active  { color: #900; }

Chromium 141 でマウスを乗せたときの文字色を取得した結果です。

リンク ホバー中の color 説明
a.bad rgb(91, 0, 0) あとに書いた :link が勝ち、色が変わらない
a.good rgb(90, 0, 0) :hover の色になる

:focus-within で入力欄のまわりを目立たせる

:focus-within は、子孫のどれかにフォーカスがあるとき、親側に一致します。入力欄を囲むラベルや枠の見た目を変えるのに便利です。

<div class="field"><label>名前 <input type="text"></label></div>
.field:focus-within { outline: 2px solid #06c; }

入力欄をクリックしたあと、.field に対して matches(":focus") は false、matches(":focus-within") は true になり、アウトラインが none から solid に変わることを確認しました。

:focusと:focus-visibleの違い

:focus は「フォーカスがあれば常に」一致し、:focus-visible は「フォーカスの目印を見せたほうがよいとブラウザが判断したとき」だけ一致します。 一般に、キーボード操作でフォーカスが移ったときと、文字を入力する欄にフォーカスがあるときに一致します。

次のHTMLで、マウスとキーボードそれぞれの操作後に matches() の結果とアウトラインを取得しました。

<button class="fb">:focus で枠</button>
<button class="fv">:focus-visible で枠</button>
<input class="fv">
.fb:focus { outline: 3px solid rgb(255, 0, 0); }

.fv { outline: none; }
.fv:focus-visible { outline: 3px solid rgb(0, 0, 255); }
操作 :focus :focus-visible 表示されたアウトライン
button.fb をマウスでクリック true false 赤い枠が出る(:focus のため)
button.fv をマウスでクリック true false 枠は出ない
Tab キーで button.fv へ移動 true true 青い枠が出る
input.fv をマウスでクリック true true 青い枠が出る
マウス操作のあと、スクリプトの focus() で button.fv へ true false 枠は出ない

(Chromium 141 での結果です。)

表から、次の使い分けが見えてきます。

  • ボタンやリンクは :focus-visible にすると、マウスでクリックしたときは枠が出ず、キーボードで移動したときだけ枠が出ます。
  • 入力欄はマウスでクリックしても :focus-visible に一致します。文字を打つ場所がどこか、常に見えているほうが親切だからです。
  • outline: none だけを書いて代わりの目印を用意しないと、キーボードで操作する人は現在位置を見分けにくくなります。消すなら :focus-visible で別の目印を必ず用意します。

コピペ用の基本形です。

/* フォーカスの目印:キーボード操作のときだけ表示 */
:focus-visible {
  outline: 3px solid #0a66c2;
  outline-offset: 2px;
}

構造の疑似クラス一覧(first-child・nth-childなど)

HTMLの並び順や親子関係で一致するものです。

疑似クラス 一致する条件 コード例 対応バージョン
:first-child 兄弟の中で最初 li:first-child { margin-top: 0; } C4 / E12 / F3 / S3.1
:last-child 兄弟の中で最後 li:last-child { border-bottom: 0; } C1 / E12 / F1 / S3.1
:only-child 兄弟がいない .tag:only-child { margin: 0; } C2 / E12 / F1.5 / S3.1
:nth-child(式) 前から数えて式に合う tr:nth-child(odd) { background: #fafafa; } C1 / E12 / F3.5 / S3.1
:nth-last-child(式) 後ろから数えて式に合う li:nth-last-child(-n+2) { color: #666; } C4 / E12 / F3.5 / S3.1
:nth-child(式 of S) S に一致する兄弟だけを数える li:nth-child(2 of .on) { color: #c00; } C111 / F113 / S9
:first-of-type 同じタグの兄弟の中で最初 p:first-of-type { font-size: 1.1em; } C1 / E12 / F3.5 / S3.1
:last-of-type 同じタグの兄弟の中で最後 p:last-of-type { margin-bottom: 0; } C1 / E12 / F3.5 / S3.1
:nth-of-type(式) 同じタグの兄弟の中で式に合う img:nth-of-type(2n) { float: right; } C1 / E12 / F3.5 / S3.1
:only-of-type 同じタグの兄弟がいない h2:only-of-type { border: 0; } C1 / E12 / F3.5 / S3.1
:empty 子要素もテキストもない .msg:empty { display: none; } C1 / E12 / F1 / S3.1
:root 文書のルート(html) :root { --main: #06c; } C1 / E12 / F1 / S1

:empty は、中身がコメントだけなら一致し、空白1文字でも入っていると一致しませんでした(Chromium 141 で <div></div> は true、<div> </div> は false、<div><!-- c --></div> は true)。テンプレートで改行や空白が入ると効かなくなるので、出力されるHTMLを確かめると確実です。

nth-childの式の対応表(2n+1・-n+3・n of S)

:nth-child(An+B) の n には 0, 1, 2, 3… が順に入り、計算結果の番号(1始まり)の要素に一致します。10個の <li> を並べ、各セレクタがどの番号に一致したかを Chromium 141 で取得しました。

<ul id="list">
  <li>1</li><li class="on">2</li><li>3</li><li class="on">4</li><li class="on">5</li>
  <li>6</li><li class="on">7</li><li>8</li><li class="on">9</li><li>10</li>
</ul>
セレクタ 一致した番号 意味
li:nth-child(3) 3 3番目だけ
li:nth-child(odd) 1, 3, 5, 7, 9 奇数番目
li:nth-child(2n+1) 1, 3, 5, 7, 9 odd と同じ
li:nth-child(even) 2, 4, 6, 8, 10 偶数番目
li:nth-child(2n) 2, 4, 6, 8, 10 even と同じ
li:nth-child(3n) 3, 6, 9 3の倍数
li:nth-child(3n+1) 1, 4, 7, 10 3個ごとの1番目(3列の左端)
li:nth-child(4n-1) 3, 7 4個ごとの3番目
li:nth-child(-n+3) 1, 2, 3 最初の3個
li:nth-child(n+4) 4〜10 4番目以降すべて
li:nth-child(n+3):nth-child(-n+6) 3, 4, 5, 6 3〜6番目(範囲)
li:nth-child(-2n+6) 2, 4, 6 6番目までの偶数
li:nth-last-child(1) 10 最後(:last-child と同じ)
li:nth-last-child(-n+2) 9, 10 最後の2個
li:not(:first-child):not(:last-child) 2〜9 最初と最後を除く

式の読み方は次のとおりです。

形 読み方 例
An A個おき(Aの倍数) 3n → 3, 6, 9
An+B B番目から、A個おき 3n+1 → 1, 4, 7
n+B B番目以降すべて n+4 → 4, 5, 6…
-n+B 最初からB番目まで -n+3 → 1, 2, 3
n+B と -n+C を重ねる B番目からC番目まで :nth-child(n+3):nth-child(-n+6)

「最初のN個」は -n+N、「N番目以降」は n+N の2つを覚えておくと、ほとんどの場面に対応できます。

「n of S」で class を持つ要素だけを数える

li.on:nth-child(2) と書くと「2番目の li で、かつ .on を持つもの」という意味になります。「.on を持つものの中で2番目」を選びたいときは、of セレクタ を使います。

セレクタ 一致した番号 意味
li.on:nth-child(2) 2 全体で2番目、かつ .on
li:nth-child(2 of .on) 4 .on の中で2番目
li.on:nth-child(odd) 5, 7, 9 全体で奇数番目、かつ .on
li:nth-child(odd of .on) 2, 5, 9 .on の中で奇数番目(1・3・5個目)
li:nth-child(-n+3 of .on) 2, 4, 5 .on の中で最初の3個
li:nth-last-child(2 of .on) 7 .on の中で後ろから2番目

.on は 2・4・5・7・9 番目に付いています。of .on を付けると、この5個だけを対象に数え直していることがわかります。

of S の構文は Chrome 111・Firefox 113・Safari 9 から使えます(@mdn/browser-compat-data 8.1.4)。絞り込み表示で一部の行を hidden にしたテーブルでも、tr:nth-child(even of :not([hidden])) と書けば、見えている行だけでしま模様を保てます(MDN の :nth-child() のページに同じ形の例があります)。

nth-childとnth-of-typeの取り違えを直す

「最初の段落だけ太字にしたいのに効かない」という場面を再現します。

<div id="art">
  <h2>見出し</h2>            <!-- 1番目の子 -->
  <p class="lead">段落1</p>  <!-- 2番目の子 -->
  <p>段落2</p>               <!-- 3番目の子 -->
  <div class="box">箱1</div> <!-- 4番目の子 -->
  <p class="box">段落3</p>   <!-- 5番目の子 -->
  <div class="box">箱2</div> <!-- 6番目の子 -->
</div>

#art の直下の子に対して、各セレクタが何番目の子に一致したかを取得しました。

セレクタ 一致した子の番号 説明
p:nth-child(1) (なし) 1番目の子は h2 なので、p という条件に合わない
p:first-child (なし) 同じ理由
p:nth-of-type(1) 2 p だけを数えて1番目
p:first-of-type 2 同上
p:nth-child(2) 2 2番目の子が p なので一致
p:last-child (なし) 最後の子は div
p:last-of-type 5 p だけを数えて最後

:nth-child は「すべての兄弟の中で何番目か」を数え、そのうえでタグや class の条件を確かめます。:nth-of-type は「同じタグの兄弟の中で何番目か」を数えます。

/* 直す前:一致する要素がない */
#art p:nth-child(1) { font-weight: 700; }

/* 直したあと:p の中で最初のものに一致 */
#art p:nth-of-type(1) { font-weight: 700; }

nth-of-typeは class ではなくタグの種類で数える

もう1つの取り違えが、「.box の2番目」を .box:nth-of-type(2) で選ぼうとする書き方です。

セレクタ 一致した子の番号 説明
.box:nth-of-type(1) 4 div の中で1番目、かつ .box
.box:nth-of-type(2) 6 div の中で2番目、かつ .box(5番目の p.box は p として3番目)
.box:nth-child(1 of .box) 4 .box の中で1番目
.box:nth-child(2 of .box) 5 .box の中で2番目

:nth-of-type の「type」はタグの種類のことで、class は数え方に関係しません。class を基準に数えたいときは :nth-child(n of .class) を使います。

やりたいこと 使う疑似クラス
並び順そのもので選ぶ(リストやテーブルの行) :nth-child()
見出しと段落が混ざる中で、段落だけを数える :nth-of-type()
特定の class を持つものだけを数える :nth-child(n of .class)

フォームの疑似クラス一覧(checked・disabled・invalidなど)

疑似クラス 一致する条件 コード例 対応バージョン
:checked チェックボックス・ラジオ・option が選択されている input:checked + span { font-weight: 700; } C1 / E12 / F1 / S3.1
:disabled / :enabled 操作できない/できる button:disabled { cursor: not-allowed; } C1 / E12 / F1 / S3.1
:required / :optional 必須/任意 input:required { border-left: 3px solid #c00; } C10 / E12 / F4 / S5
:valid / :invalid 入力内容が検証に通る/通らない input:invalid { border-color: #c00; } C10 / E12 / F4 / S5
:user-valid / :user-invalid ユーザーが操作したあとで検証に通る/通らない input:user-invalid { border-color: #c00; } C119 / E119 / F88 / S16.5
:placeholder-shown プレースホルダーが表示されている(未入力) input:placeholder-shown { background: #fafafa; } C47 / E79 / F51 / S9
:read-only / :read-write 編集できない/できる input:read-only { background: #eee; } C1 / E13 / F78 / S4
:in-range / :out-of-range min・max の範囲内/範囲外 input:out-of-range { outline: 1px solid #c00; } C10 / E13 / F29 / S5.1
:indeterminate チェックボックスが未確定の状態など input:indeterminate { opacity: .6; } C1 / E12 / F2 / S3
:default 既定の選択・既定の送信ボタン button:default { font-weight: 700; } C10 / E79 / F4 / S5
:autofill ブラウザが自動入力した input:autofill { border-color: #06c; } C110 / E110 / F86 / S15

:invalid と :user-invalid の違い

required を付けた入力欄は、何も入力していない最初の時点から :invalid に一致します。ページを開いた瞬間に赤い枠が並ぶのを避けたいときは、ユーザーが操作したあとにだけ一致する :user-invalid が向いています。

<form>
  <input type="email" required placeholder="mail">
  <button type="button">送信</button>
</form>

Chromium 141 で、操作ごとの一致を確かめました。

操作 :invalid :user-invalid :placeholder-shown
ページを開いた直後(未入力) true false true
abc と入力してフォーカスを外す true true false
a@example.com と入力し直してフォーカスを外す false(:valid と :user-valid が true) false false
/* 操作したあとで、検証に通らない欄だけ色を変える */
input:user-invalid { border-color: #c00; }
input:user-valid   { border-color: #2a7; }

:checked と :has() でラベル全体の見た目を変える

<label><input type="checkbox"> 同意する</label>
label:has(input:checked) { font-weight: 700; }

チェック前の font-weight は 400、チェック後は 700 になることを確認しました。:checked は入力欄そのものにしか一致しないため、囲んでいる label を変えたいときは次の章の :has() と組み合わせます。

関数型の疑似クラス一覧(not・is・where・has)

かっこの中にセレクタを受け取る疑似クラスです。

疑似クラス 意味 詳細度 コード例 対応バージョン
:not(S) S に一致しない 引数の中でいちばん高いもの a:not([href^="http"]) C1 / E12 / F1 / S3.1(複数指定は C88 / F84 / S9)
:is(S) S のどれかに一致 引数の中でいちばん高いもの :is(h1, h2, h3) + p C88 / E88 / F78 / S14
:where(S) S のどれかに一致 常に 0 :where(ul, ol) { padding: 0; } C88 / E88 / F78 / S14
:has(S) S に一致する子孫・兄弟を持つ 引数の中でいちばん高いもの .card:has(img) C105 / E105 / F121 / S15.4
:lang(言語) 指定の言語の要素 0,1,0 :lang(en) { font-family: serif; } C1 / E12 / F1 / S3.1
:dir(方向) 文字の方向(ltr・rtl) 0,1,0 :dir(rtl) { text-align: right; } C120 / E120 / F49 / S16.4

:not() の使い方

/* .done 以外の項目 */
li:not(.done) { color: #222; }

/* 最後以外に区切り線を引く */
li:not(:last-child) { border-bottom: 1px solid #ddd; }

/* 複数を除く(カンマ区切り) */
#nl span:not(.a, .b) { color: rgb(70, 0, 0); }

<span class="a">・<span class="b">・<span class="c">・<span> の4つに3つ目のルールを当てると、色が付いたのは3番目と4番目だけでした(Chromium 141)。:not(.a, .b) は「.a でも .b でもない」という意味になります。

:not() の詳細度は引数で決まる点も覚えておくと役立ちます。

.x3:not(#nothing) { color: rgb(3, 3, 3); } /* 0,1,0 + 1,0,0 = 1,1,0 */
.x3.x3.x3         { color: rgb(4, 4, 4); } /* 0,3,0 */

あとに書いた .x3.x3.x3 ではなく、rgb(3, 3, 3) が適用されました。#nothing というIDの要素が実在しなくても、:not() の中にIDを書いた時点で、IDぶんの詳細度が加わります。

:is() でセレクタをまとめる

/* まとめる前 */
article h1 a, article h2 a, article h3 a { color: inherit; }

/* :is() でまとめる */
article :is(h1, h2, h3) a { color: inherit; }

:is() と :where() には、引数の中に解釈できないセレクタが混ざっても、ほかの部分は有効なままという特徴があります。通常のカンマ区切りでは、1つでも解釈できないものがあるとルール全体が無効になります。

.g1, .g1:unknown-thing { color: rgb(50, 0, 0); }     /* ルール全体が無効 */
:is(.g2, .g2:unknown-thing) { color: rgb(60, 0, 0); } /* .g2 には適用される */
要素 取得した color 結果
.g1 rgb(0, 0, 0) 適用されない
.g2 rgb(60, 0, 0) 適用される

:where() の詳細度は0(デモつき)

:where() は :is() と同じように一致しますが、かっこの中にIDを書いても詳細度は 0 です。次の2つのルールで比べます。

<div id="w1"><p class="note">w1</p></div>
<div id="w2"><p class="note note2">w2</p></div>
/* w1::where() の中の ID は数えない */
#w1 p             { color: rgb(0, 0, 255); } /* 1,0,1 */
:where(#w1) p.note { color: rgb(255, 0, 0); } /* 0,1,1(あとに書いている) */

/* w2::is() は引数の中でいちばん高い詳細度を持つ */
#w2 :is(p, #dummy) { color: rgb(0, 128, 0); } /* 1,0,0 + 1,0,0 = 2,0,0 */
#w2 p.note.note2   { color: rgb(255, 0, 0); } /* 1,2,1(あとに書いている) */
要素 取得した color 勝ったルール
#w1 p rgb(0, 0, 255)(青) #w1 p。あとに書いた :where(#w1) p.note は 0,1,1 なので届かない
#w2 p rgb(0, 128, 0)(緑) #w2 :is(p, #dummy)。#dummy のぶんが加わり 2,0,0 になる

:where() は「あとから上書きしやすい土台のスタイル」に向いています。 リセットCSSや共通部品の初期値を :where() で書いておくと、利用する側は class 1つで上書きできます。

/* 土台:詳細度 0 なので、どんなセレクタでも上書きできる */
:where(ul, ol)[role="list"] { list-style: none; padding: 0; }
:where(a) { color: #06c; }

/* 利用側:class 1つで上書き */
.nav-link { color: #222; }

:is() は、引数に書いた中でいちばん強いセレクタの詳細度が、実際にどれへ一致したかに関係なく採用されます。#w2 の例では、一致したのは p ですが、詳細度は #dummy のぶんで計算されました。

:has() で親要素を選ぶ

:has() は、「〜を中に持つ要素」「直後に〜が続く要素」を選べます。CSSだけで親や前の兄弟を選べるようになったのが大きな特徴です。

<div class="card" id="c1"><img alt=""><p>画像あり</p></div>
<div class="card" id="c2"><p>画像なし</p></div>

<h2 id="h2a">A</h2><p>直後に p</p>
<h2 id="h2b">B</h2><div>直後は div</div>
.card           { border-top: 1px solid; }
.card:has(img)  { border-top-width: 5px; }  /* 画像を持つカードだけ */

h2          { margin-bottom: 20px; }
h2:has(+ p) { margin-bottom: 4px; }          /* 直後が p の見出しだけ */

form:has(:invalid) button { opacity: 0.5; }  /* 検証に通らない欄があるフォームのボタン */
要素 取得した値 説明
#c1(画像あり) border-top-width: 5px :has(img) に一致
#c2(画像なし) border-top-width: 1px 一致しない
#h2a(直後が p) margin-bottom: 4px :has(+ p) に一致
#h2b(直後が div) margin-bottom: 20px 一致しない
フォームのボタン(未入力/正しく入力後) opacity: 0.5 → 1 :invalid の欄がなくなると戻る

:has() は MDN で「Baseline Widely available(2023年12月から主要ブラウザで利用可能)」と示されています。対応バージョンは Chrome 105・Edge 105・Firefox 121・Safari 15.4 です。知っておきたい決まりは次のとおりです。

  • :has() の中に :has() を入れ子にはできません(Chromium 141 で CSS.supports("selector(:has(:has(a)))") は false)。
  • 詳細度は :is() や :not() と同じく、引数の中でいちばん高いものになります(.h:has(#inner) が .h.h.h より優先されることを確認)。
  • MDN では、body や :root のような広い要素を起点にするより、.card のように範囲を絞った要素を起点にすることがすすめられています。

疑似クラスの詳細度まとめ

詳細度は「ID の数, class・属性・疑似クラスの数, タグ・疑似要素の数」の3つ組で比べます。左の数字から順に大きいほうが優先され、同じなら、あとに書いたルールが使われます。

セレクタ 詳細度 数え方
a:hover 0,1,1 疑似クラス1 + タグ1
.btn:hover:focus-visible 0,3,0 class 1 + 疑似クラス2
li:nth-child(2n+1) 0,1,1 疑似クラス1 + タグ1
li:nth-child(2 of .on) 0,2,1 疑似クラス1 + of の引数(class 1)+ タグ1
li:not(.done) 0,1,1 :not 自体は 0、引数の class 1 + タグ1
:is(h1, #main) a 1,0,1 引数でいちばん高い ID 1 + タグ1
:where(#main) a 0,0,1 :where は 0 + タグ1
.card:has(img) 0,1,1 class 1 + 引数のタグ1
p::first-line 0,0,2 タグ1 + 疑似要素1

「:where() は 0」「:is()・:not()・:has() は引数でいちばん高いもの」「そのほかの疑似クラスは class と同じ」の3つで整理できます。:nth-child(n of S) は、疑似クラス自身のぶんに S の詳細度が加わります(#n li:nth-child(1 of #first) が #n li.z.z.z より優先されることを確認)。

スマホでhoverが残る問題を再現して直す

タッチ端末にはマウスカーソルがないため、タップした要素に :hover が一致したまま残ることがあります。Playwright の Pixel 7 の設定(タッチあり)で Chromium 141 を動かして再現しました。

/* 直す前 */
.hb { background-color: rgb(255, 255, 255); }
.hb:hover { background-color: rgb(255, 200, 0); }
タイミング .hb の背景色 matches(":hover")
タップ前 rgb(255, 255, 255) false
タップして 0.5 秒後 rgb(255, 200, 0) true
別のボタンをタップしたあと rgb(255, 255, 255) false

タップから 0.5 秒たっても背景色がホバー時の色のままで、別の場所をタップするまで戻りませんでした。ボタンが「押されたまま」に見える原因がこれです。

修正版:@media (hover: hover) で囲む

ホバーの見た目を、ホバーできる端末だけに適用します。

/* 直したあと */
.hb2 { background-color: rgb(255, 255, 255); }

@media (hover: hover) and (pointer: fine) {
  .hb2:hover { background-color: rgb(255, 200, 0); }
}

/* タッチ端末でも「押した手ごたえ」を出す */
.hb2:active { background-color: rgb(255, 225, 120); }

同じ Pixel 7 の設定で確かめた結果です。

項目 結果
matchMedia("(hover: hover)") false
matchMedia("(hover: none)") true
matchMedia("(pointer: coarse)") true
.hb2 をタップして 0.5 秒後の背景色 rgb(255, 255, 255)(白のまま)
そのときの matches(":hover") true

:hover 自体はタップ後も一致していますが、メディアクエリの条件に合わないため、ホバー用のスタイルは適用されません。パソコン向けの設定((hover: hover) と (pointer: fine) がどちらも true)では、マウスを乗せると .hb2 も rgb(255, 200, 0) に変わることを確認しています。:active の行は今回の計測には含めていません(未実行)。実機の Safari など、ほかのブラウザでの動きは今回は確かめていません。

ホバーでしか見えない情報(メニューや説明文)を作らないことも大切です。タッチ端末やキーボードでも同じ内容にたどり着けるよう、:focus-visible や :focus-within にも同じスタイルを当てておくと安心です。

@media (hover: hover) and (pointer: fine) {
  .menu:hover > .submenu { display: block; }
}
.menu:focus-within > .submenu { display: block; }

やりたいこと別の逆引き表

やりたいこと セレクタ
マウスを乗せたときに色を変える .btn:hover
キーボード操作のときだけ枠を出す .btn:focus-visible
入力欄にフォーカスがある間、囲みを目立たせる .field:focus-within
テーブルを1行おきに色分けする tr:nth-child(even)
最初の3個だけ選ぶ li:nth-child(-n+3)
4番目以降を選ぶ li:nth-child(n+4)
最後の項目だけ線を消す li:last-child または li:not(:last-child) に線を引く
3列グリッドの左端を選ぶ .item:nth-child(3n+1)
特定の class の中でN番目を選ぶ li:nth-child(N of .on)
見出しと混ざった中で最初の段落を選ぶ p:first-of-type
中身が空の要素を隠す .msg:empty
チェックされた項目のラベルを変える label:has(input:checked)
操作後に検証に通らない欄を示す input:user-invalid
未入力の欄を見分ける input:placeholder-shown
特定の class 以外を選ぶ li:not(.done)
複数の見出しをまとめて指定する :is(h1, h2, h3)
上書きしやすい初期スタイルを書く :where(ul, ol)
画像を持つカードだけ変える .card:has(img)
ページ内リンクの移動先を目立たせる section:target

よくあるつまずきと直し方

起きること 原因 直し方
p:nth-child(1) が効かない 1番目の子が p ではない p:nth-of-type(1) にする
.box:nth-of-type(2) が思った要素に当たらない タグの種類で数えている :nth-child(2 of .box) にする
a:hover の色が変わらない :link をあとに書いている L→V→H→A の順に並べる
スマホでホバーの色が残る タップ後も :hover が一致している @media (hover: hover) で囲む
クリックしただけでボタンに枠が出る :focus に枠を付けている :focus-visible にする
開いた直後から入力欄が赤い required の欄は最初から :invalid :user-invalid にする
:not(#id) を足したら上書きできなくなった :not の引数の詳細度が加わる :where() で包む、または class で書く
.msg:empty が効かない 空白や改行が入っている HTMLの出力から空白を取り除く
a :hover と書いたら中の要素すべてに効いた a :hover(スペースあり)は子孫セレクタになる スペースを入れずに a:hover と書く

よくある質問

Q. 疑似クラスと疑似要素の違いは何ですか? A. 疑似クラスは :hover のようにコロン1つで書き、すでにある要素を状態や位置で絞り込みます。疑似要素は ::before のようにコロン2つで書き、要素の一部分やHTMLにない部分を対象にします。詳細度は、疑似クラスが class と同じ、疑似要素がタグと同じです。

Q. nth-childとnth-of-typeはどう使い分けますか? A. 同じ種類の要素だけが並ぶリストやテーブルの行には :nth-child、見出しと段落のように違うタグが混ざる中で特定のタグだけを数えたいときは :nth-of-type を使います。class を基準に数えたいときは :nth-child(n of .class) が使えます。

Q. :focusと:focus-visibleはどちらを使えばよいですか? A. ボタンやリンクの枠線には :focus-visible が向いています。本記事の確認では、マウスでクリックしたボタンは :focus だけに一致し、Tab キーで移動したときに :focus-visible にも一致しました。入力欄はマウスでクリックしても :focus-visible に一致します。

Q. :is()と:where()の違いは何ですか? A. 一致する要素は同じで、詳細度だけが違います。:is() は引数の中でいちばん高い詳細度を持ち、:where() は常に 0 です。あとから上書きされる前提の初期スタイルには :where()、通常のまとめ書きには :is() が使いやすい選択です。

まとめ

  • 疑似クラスはコロン1つで書き、要素の状態や位置を条件にスタイルを当てます。 詳細度は class と同じ 0,1,0 が基本です。
  • :nth-child(-n+3) は最初の3個、:nth-child(n+4) は4番目以降、:nth-child(2 of .on) は .on の中の2番目に一致します。
  • :nth-child はすべての兄弟で数え、:nth-of-type は同じタグの兄弟で数えます。 class 基準で数えるなら of セレクタ を使います。
  • ボタンやリンクのフォーカス枠は :focus-visible にすると、キーボード操作のときだけ表示されます。
  • :where() の詳細度は 0、:is()・:not()・:has() は引数でいちばん高いものになります。
  • :has() を使うと、子や直後の兄弟を条件に親側の要素を選べます。
  • スマートフォンでホバーの見た目が残るときは、@media (hover: hover) and (pointer: fine) で囲みます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「CSS疑似クラス一覧|hover・nth-child・focus・notの使い方」

株式会社RJCでは、CSSの疑似クラスやセレクタの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?