CSSの:is()・:has()・:where()を使いこなす|セレクタ設計が変わる3つの疑似クラス

CSSのセレクタを書いていて、

「同じような記述を何度も繰り返している」

「親要素の状態で子のスタイルを変えたいけど、JSを使うしかない」

と感じたことはありませんか?

そのモヤモヤを解消してくれるのが、:is():has():where()の3つの疑似クラスです。

これらはセレクタの書き方を根本から変えるポテンシャルを持っており、

うまく使えばCSSの保守性と可読性が大幅に上がります。

ただし、それぞれ詳細度(specificity)の扱いが微妙に違うため、

なんとなくで使うとスタイルが思い通りに当たらない落とし穴にはまることも。

この記事では、動作原理と詳細度の違いをしっかり整理した上で、実務で使える応用パターンまで解説します。

おねしゃす


目次

:is() ── セレクタのグルーピングと詳細度の引き継ぎ

基本の動作

:is()は複数のセレクタをひとつにまとめて書ける機能です。

css

CSS
/* 従来の書き方 */
header h1,
header h2,
header h3,
main h1,
main h2,
main h3 {
  color: #333;
}

/* :is()を使った書き方 */
:is(header, main) :is(h1, h2, h3) {
  color: #333;
}
CSS

見た目の違いは明らかですが、ここで重要なのは詳細度の扱いです。

:is()の詳細度ルール

:is()の詳細度は、カッコ内のセレクタのうち最も詳細度が高いものを引き継ぎます。これが思わぬ挙動を生む原因になります。

css

CSS
:is(.highlight, p) {
  color: blue;
}

p {
  color: red;
}
CSS

このコードで <p> 要素の文字色はどうなるでしょうか。:is(.highlight, p).highlight(クラスセレクタ)の詳細度を引き継ぐため、クラスが付いていない <p> に対しても詳細度は (0,1,0) になります。一方、後ろの p(0,0,1) なので、:is() の方が勝って blue になります。

カッコ内のセレクタが全部要素セレクタなら詳細度は低いまま、IDセレクタが混じれば一気に詳細度が上がる——という点を必ず意識しておきましょう。

NGパターンとOKパターン

誤解しやすいのが、「:is()でまとめれば詳細度が下がって上書きしやすくなる」という思い込みです。

css

CSS
/* NG:IDセレクタが混じっているため詳細度が爆発する */
:is(#header, .nav, p) {
  font-size: 1rem;
}

/* OK:詳細度を揃えたセレクタだけまとめる */
:is(.header, .nav, p) {
  font-size: 1rem;
}
CSS

詳細度の異なるセレクタを混在させると、意図しない詳細度が全体に波及します。:is()の中にはできるだけ詳細度が揃ったセレクタを入れるのが安全な使い方です。


:where() ── 詳細度ゼロで設計するベーススタイルの定石

:is()との違いは詳細度だけ

:where():is()と書き方は全く同じですが、詳細度が常にゼロになります。

css

CSS
:where(header, main) :where(h1, h2, h3) {
  color: #333;
}
CSS

この規則のポイントは「詳細度がゼロ=どこからでも上書きできる」ということです。

実務での活用:デザインシステムのベーススタイル

:where()が特に力を発揮するのは、コンポーネントライブラリやデザインシステムのリセット・ベーススタイルを定義するときです。

css

CSS
/* :where()でベーススタイルを定義(詳細度ゼロ) */
:where(h1, h2, h3, h4) {
  font-weight: 700;
  line-height: 1.3;
  margin-bottom: 0.5em;
}

:where(p, ul, ol) {
  margin-bottom: 1em;
  line-height: 1.7;
}
CSS

このように書いておくと、使う側はシンプルなセレクタで自由に上書きできます。

css

CSS
/* 詳細度ゼロに対してどんなセレクタでも勝てる */
.article h2 {
  font-size: 1.5rem;
  color: #0055cc;
}
CSS

:is() でベーススタイルを書いてしまうと、上書きしたいときに詳細度の競合が発生することがあります。「ライブラリ・リセット側は:where()、コンポーネント側は通常のセレクタ」という設計が、最もスケールしやすいパターンです。

:is()と:where()の使い分けまとめ

:is():where()
詳細度カッコ内の最大値を引き継ぐ常にゼロ
向いている用途通常のスタイル定義・セレクタの整理ベーススタイル・リセットCSS・ライブラリ
上書きのしやすさ詳細度次第どこからでも上書き可能

:has() ── CSSに「親セレクタ」がついに登場

なぜ:has()が革命的なのか

CSSには長らく「親要素を選択する方法」がありませんでした。子要素の状態に応じて親のスタイルを変えたい場合、JavaScriptでクラスを付け外しするしかなかったのです。:has()はその制約を崩します。

css

CSS
/* imgを含むfigureにだけ余白を広くとる */
figure:has(img) {
  padding: 2rem;
}
CSS

figureの中にimgがあれば、そのfigureに対してスタイルを当てる」という意味です。

実務で使える:has()のパターン

フォームのバリデーション状態と連動させる

css

CSS
/* エラー状態のinputを含むフォームグループ全体を赤くする */
.form-group:has(input:invalid) {
  border-left: 3px solid #e53e3e;
  padding-left: 1rem;
}

.form-group:has(input:invalid) label {
  color: #e53e3e;
  font-weight: bold;
}
CSS

inputの状態変化に連動して、ラベルや親ラッパーまで一括でスタイルが変わります。JavaScriptのクラス操作が不要になるシーンです。

カードレイアウトの画像あり・なし対応

css

CSS
.card:has(img) {
  grid-template-rows: auto 1fr;
}

/* :not(:has())で「含まない」場合も制御できる */
.card:not(:has(img)) {
  padding-top: 2rem;
}
CSS

ナビゲーションの状態管理

css

CSS
/* チェックボックスにチェックが入ったらメニューを開く(JS不要) */
.nav:has(input[type="checkbox"]:checked) .nav-menu {
  display: block;
}
CSS

ハンバーガーメニューのような動的UIをCSSだけで実装できます。

:has()の詳細度

:has()の詳細度は:is()と同様で、カッコ内のセレクタの詳細度を引き継ぎます

css

CSS
/* :has(img)はimg要素セレクタの詳細度(0,0,1)を引き継ぐ */
figure:has(img) { ... }  /* 詳細度:(0,0,2) */

/* :has(.active)はクラスセレクタの詳細度(0,1,0)を引き継ぐ */
figure:has(.active) { ... }  /* 詳細度:(0,1,1) */
CSS

ブラウザ対応状況

2024年末時点で、Chrome・Firefox・Safari・Edgeのいずれも:has()に対応しています。IE11は非対応ですが、現在の多くのプロジェクトではIEサポートを終了しているため、実務投入のハードルは下がっています。対応ブラウザを確認した上で積極的に使っていきましょう。


3つを組み合わせた応用パターン

それぞれを単体で使うだけでなく、組み合わせることで表現力が増します。

css

CSS
/* 画像を含むsectionの中にあるh2かh3に特定のスタイルを当てる */
section:has(img) :is(h2, h3) {
  font-size: 1.4rem;
  border-bottom: 2px solid #0055cc;
}
CSS

css

CSS
/* ベーススタイルは:where()で詳細度ゼロにしつつ、
   特定の条件下では:is()で通常の詳細度でスタイルを上書き */
:where(.card, .panel) {
  padding: 1rem;
  border-radius: 4px;
}

:is(.card, .panel):has(img) {
  padding-top: 0;
}
CSS

「デフォルトは:where()で詳細度ゼロ、条件付きの例外は:is()や通常セレクタで詳細度を持たせる」という設計にすると、スタイルの優先順位が整理しやすくなります。


よくある落とし穴

疑似要素は:is()・:where()に入れられない

css

CSS
/* NG */
:is(::before, ::after) { content: ""; }

/* OK:疑似要素は個別に書く */
::before, ::after { content: ""; }
CSS

:has()は現時点でネストした疑似クラスのサポートが限定的

ブラウザによっては :has(:not(...)) のような複雑なネストで挙動が不安定な場合があります。シンプルな構造を心がけ、実機確認を忘れずに。


まとめ

3つの疑似クラスの使い分けをシンプルに整理するとこうなります。

  • :is() ── セレクタのグルーピングに使う。詳細度はカッコ内の最大値を引き継ぐ
  • :where() ── 詳細度ゼロでまとめたいときに使う。ライブラリやベーススタイル設計に最適
  • :has() ── 子要素の状態で親を選択できる。JSのクラス操作を減らせる

詳細度の挙動さえ把握してしまえば、3つとも実務で使い勝手のよい機能です。特に:has()はCSSの設計思想そのものを変える可能性があり、対応ブラウザが揃った今こそ積極的に取り入れてみる価値があります。

まずは手元のプロジェクトで「繰り返しが多いセレクタを:is()にまとめる」ところから始めてみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次