🎯

詳細度は合計じゃない!階層で理解する:where() vs :is()

に公開
2

はじめに:詳細度との戦い

CSS を書いていて、こんな経験ありませんか?

/* せっかく書いたスタイルが効かない... */
.button {
  background: blue;
}

/* 詳細度が高いから上書きされる */
header .button {
  background: red;
}

詳細度(Specificity) はCSS設計の永遠の悩みです。

特に、生成AIがコードを書くようになってから、こんなコードをよく見かけるようになりました:

/* 生成AIがよく書くコード */
:is(.nav, .sidebar) .link {
  color: blue;
}

:where(.nav, .sidebar) .link {
  color: red;
}

どっちの色になるか、即答できますか?

実は、:is():where()見た目は同じでも、詳細度が全く違います

この記事では、中級者でも知らない人が多い「:where() vs :is()」の詳細度の違いを完全解説します。


そもそも「詳細度」とは?

まず、:is():where() の違いを理解する前に、詳細度(Specificity) について簡単におさらいしましょう。

詳細度は「強さ」を表す数値

CSSでは、同じ要素に複数のスタイルが適用された場合、どれを優先するかを決めるルールがあります。

/* どっちの色になる? */
p {
  color: blue;
}

p {
  color: red;
}

答え:red(後に書かれた方が優先)

でも、こういう場合は?

p {
  color: blue;
}

.text {
  color: red;
}
<p class="text">このテキストは何色?</p>

答え:red(クラスセレクタの方が「強い」から)

この「強さ」を数値化したものが詳細度です。

詳細度の計算方法

詳細度は (a, b, c) の3つの数値で表されます:

セレクタ a(ID) b(クラス、属性、疑似クラス) c(要素、疑似要素) 詳細度
p 0 0 1 (0, 0, 1)
.text 0 1 0 (0, 1, 0)
#main 1 0 0 (1, 0, 0)
p.text 0 1 1 (0, 1, 1)
#main .text p 1 1 1 (1, 1, 1)

重要:詳細度は合計点ではありません!

詳細度の比較は、3桁の数字のように左から順に比較します。

(0, 1, 0)  と  (0, 0, 1)  を比較
 ↓  ↓  ↓      ↓  ↓  ↓
 a  b  c      a  b  c

1. aを比較: 0 = 0  → 同じなので次へ
2. bを比較: 1 > 0  → (0,1,0)の勝ち!
3. cは見ない

つまり、a階層(ID)で負けていたら、b・cがどんなに高くても意味がありません。

極端な例:

/* 詳細度: (1, 0, 0) - ID が1つ */
#main {
  color: blue;
}

/* 詳細度: (0, 100, 100) - クラス100個 + 要素100個 */
.a .b .c /* ...クラス100個省略... */ p p p /* ...要素100個省略... */ {
  color: red;
}
<p id="main" class="a b c ...">blueになる(ID1個が圧勝!)</p>

左の桁が1つでも大きければ、右の桁がどんなに大きくても勝てません。

これが「詳細度」の仕組みです。桁ごとの比較なので、階層構造になっているんですね。

詳細度が同じ場合

詳細度が同じ場合は、後に書かれた方が優先されます。

/* 詳細度: (0, 1, 0) */
.text {
  color: blue;
}

/* 詳細度: (0, 1, 0) - 同じ */
.content {
  color: red;
}
<p class="text content">redになる(後に書かれているから)</p>

なぜ詳細度が重要なのか?

詳細度を理解していないと:

/* リセットCSS */
h1, h2, h3 {
  margin: 0;
  font-weight: normal;
}

/* カスタマイズしようとして... */
h2 {
  font-weight: bold; /* 効かない! */
}

/* なぜ?詳細度が同じで、リセットCSSの方が先に書かれているから */

/* 詳細度を上げる必要がある */
.article h2 {
  font-weight: bold; /* これなら効く */
}

このように、詳細度を正しく理解しないと、思い通りのスタイルが適用されません。


:is() とは?

:is() は、複数のセレクタをまとめて書ける疑似クラスです。

基本的な使い方

/* 従来の書き方 */
.nav a:hover,
.sidebar a:hover,
.footer a:hover {
  color: red;
}

/* :is() を使った書き方 */
:is(.nav, .sidebar, .footer) a:hover {
  color: red;
}

コードが短くなって読みやすくなりますね!

ベンダープレフィックスの代替にも便利

/* 従来の書き方 */
input::-webkit-input-placeholder {
  color: gray;
}
input::-moz-placeholder {
  color: gray;
}
input:-ms-input-placeholder {
  color: gray;
}

/* :is() を使った書き方 */
input:is(::-webkit-input-placeholder, ::-moz-placeholder, :-ms-input-placeholder) {
  color: gray;
}

:where() とは?

:where():is() と同じく、複数のセレクタをまとめて書ける疑似クラスです。

/* 書き方は :is() と同じ */
:where(.nav, .sidebar, .footer) a:hover {
  color: red;
}

「じゃあ何が違うの?」

答え:詳細度が違います!


詳細度の違いを理解する

:is() の詳細度

:is() は、引数の中で最も高い詳細度を引き継ぎます

/* :is(.text, #main) の詳細度は? */
:is(.text, #main) p {
  color: red;
}

答え:(1, 0, 1)

  • .text の詳細度:(0, 1, 0)
  • #main の詳細度:(1, 0, 0)
  • 最も高い #main の詳細度 (1, 0, 0) を引き継ぐ
  • p の詳細度 (0, 0, 1) を加えて (1, 0, 1)

:where() の詳細度

:where() は、常に詳細度が 0 です。

/* :where(.text, #main) の詳細度は? */
:where(.text, #main) p {
  color: red;
}

答え:(0, 0, 1)

  • :where() の詳細度:(0, 0, 0) ← 常に0!
  • p の詳細度 (0, 0, 1) のみで (0, 0, 1)

実例で比較してみよう

パターン1::is() vs クラスセレクタ

/* 詳細度: (0, 1, 1) - クラス1 + 要素1 */
:is(.nav, .sidebar) .link {
  color: blue;
}

/* 詳細度: (0, 1, 1) - クラス1 + 要素1 */
.footer .link {
  color: red;
}
<div class="nav footer">
  <a class="link">何色?</a>
</div>

答え:red(詳細度が同じなので、後に書かれた方が優先)

パターン2::where() vs クラスセレクタ

/* 詳細度: (0, 0, 1) - 要素1のみ(:where()は0) */
:where(.nav, .sidebar) .link {
  color: blue;
}

/* 詳細度: (0, 1, 1) - クラス1 + 要素1 */
.footer .link {
  color: red;
}
<div class="nav footer">
  <a class="link">何色?</a>
</div>

答え:red.footer .link の方が詳細度が高い)

パターン3::is() with ID vs :where() with ID

/* 詳細度: (1, 0, 1) - ID1 + 要素1(:is()は#mainの詳細度を引き継ぐ) */
:is(.text, #main) p {
  color: blue;
}

/* 詳細度: (0, 0, 1) - 要素1のみ(:where()は0) */
:where(.text, #main) p {
  color: red;
}
<div class="text" id="main">
  <p>何色?</p>
</div>

答え:blue:is() が ID の詳細度を引き継いでいるから圧勝)


実用パターン1:リセットCSSで :where() を使う

従来のリセットCSS

/* リセットCSS */
h1, h2, h3 {
  margin: 0;
  font-weight: normal;
}

/* カスタマイズしようとして... */
h2 {
  font-weight: bold; /* 効かない! */
}

問題:詳細度が同じなので、後に書いても上書きできない

:where() を使ったリセットCSS

/* :where() でリセットCSS */
:where(h1, h2, h3) {
  margin: 0;
  font-weight: normal;
}

/* カスタマイズ */
h2 {
  font-weight: bold; /* 効く! */
}

解決::where() は詳細度が 0 なので、簡単に上書きできる


実用パターン2:上書き可能なコンポーネント

/* ベーススタイル(詳細度 0) */
:where(.button) {
  background: gray;
  color: white;
  padding: 10px 20px;
}

/* 簡単に上書きできる */
.primary {
  background: blue;
}

.danger {
  background: red;
}
<button class="button">グレー</button>
<button class="button primary"></button>
<button class="button danger"></button>

メリット:.button の詳細度が 0 なので、.primary だけで上書きできる


実用パターン3:ベンダープレフィックスを :is() でまとめる

/* 詳細度を維持したまま複数のプレフィックスをまとめる */
:is(:-webkit-full-screen, :-moz-full-screen, :fullscreen) {
  background: black;
}

メリット:コードが短くなり、詳細度も維持される


:is() vs :where() 使い分けチャート

用途 推奨 理由
リセットCSS :where() 詳細度を0にして、簡単に上書き可能に
ベーススタイル :where() 詳細度を下げて、柔軟なカスタマイズを可能に
複数セレクタのまとめ :is() 詳細度を維持したまま、コードを短縮
ベンダープレフィックス :is() 詳細度を維持したまま、プレフィックスをまとめる

実際に動くデモを見てみよう

デモ1:詳細度の違いを体感

デモ2:リセットCSSでの活用


ネストした場合の挙動

/* ネストした場合も同じルールが適用される */
:is(.nav, #main) :where(.link, .button) {
  color: red;
}

詳細度:(1, 0, 0)

  • :is(.nav, #main)(1, 0, 0) (#mainを引き継ぐ)
  • :where(.link, .button)(0, 0, 0) (常に0)
  • 合計:(1, 0, 0)

ブラウザサポート状況

ブラウザ :is() :where()
Chrome 88+ 88+
Firefox 78+ 78+
Safari 14+ 14+
Edge 88+ 88+

結論:2021年以降のブラウザならほぼ全てサポート

レガシーブラウザ対応

/* フォールバック */
.nav a:hover,
.sidebar a:hover {
  color: red;
}

/* モダンブラウザ用 */
@supports selector(:is(.nav)) {
  :is(.nav, .sidebar) a:hover {
    color: red;
  }
}

よくある間違い

間違い1::where() でも詳細度がある と思い込む

/* ❌ 間違い::where() にも詳細度があると思っている */
:where(#main) p {
  color: red; /* 詳細度: (0, 0, 1) */
}

.text p {
  color: blue; /* 詳細度: (0, 1, 1) → こっちが勝つ */
}

正解::where() は ID を含んでいても詳細度は 0

間違い2::is() で全ての引数の詳細度が加算される と思い込む

/* ❌ 間違い:.text と #main の詳細度が合計される? */
:is(.text, #main) p {
  color: red;
  /* 詳細度: (1, 1, 1) ではない! */
  /* 正解: (1, 0, 1) - 最も高い #main の詳細度のみ */
}

正解::is() は引数の中で最も高い詳細度を1つだけ引き継ぐ


まとめ

:is() と :where() の違い

特徴 :is() :where()
詳細度 引数の最も高い詳細度を引き継ぐ 常に 0
用途 複数セレクタのまとめ リセットCSS、ベーススタイル
上書きやすさ 通常の詳細度ルール 簡単に上書き可能

使い分けのポイント

  • 詳細度を維持したい:is()
  • 詳細度を下げたい:where()
  • リセットCSS:where()
  • ベンダープレフィックス:is()

今日から使えるチェックリスト

  • リセットCSSを :where() に置き換える
  • ベーススタイルを :where() で定義する
  • 複数セレクタを :is() でまとめる
  • ベンダープレフィックスを :is() で整理する

参考リンク


🔍 追記:読者からのフィードバック

コメント欄でご指摘いただいた通り、リセットCSSには :where() よりも @layer の方が優れているケースがあります!

@layer とは?

@layer は、CSSの優先順位をレイヤー(層)で管理できる新しい機能です。

@layer reset, base, components, utilities;

@layer reset {
  /* 詳細度に関係なく、最も弱い層 */
  body {
    margin: 0;
    padding: 0;
  }
}

@layer components {
  /* reset より強い */
  .button {
    padding: 10px;
  }
}

/* レイヤー外 = 常に最優先 */
body {
  background: white; /* resetより強い */
}

なぜリセットCSSに @layer が優れているのか?

/* :where() の場合 */
:where(h1, h2, h3) {
  margin: 0; /* 詳細度: (0, 0, 1) */
}

h1.title {
  margin: 1em; /* 詳細度: (0, 1, 1) - 勝つ */
}

/* @layer の場合 */
@layer reset {
  h1, h2, h3 {
    margin: 0; /* 詳細度: (0, 0, 1) */
  }
}

h1.title {
  margin: 1em; /* 詳細度: (0, 1, 1) だけど勝つ! */
}

/* レイヤー外 = 常に優先 */
h1 {
  margin: 1em; /* 詳細度 (0,0,1) だけど勝つ! */
}

:where() と @layer の使い分け

用途 推奨 理由
プロジェクト全体の構造管理 @layer リセット、コンポーネント、ユーティリティを明確に分離
リセットCSS @layer 詳細度から完全に独立できる
特定のセレクタだけ詳細度を下げたい :where() ピンポイントで使える
既存プロジェクトへの導入 :where() 部分的に導入しやすい

まとめ

  • 小規模・既存プロジェクト: :where() で十分
  • 大規模・新規プロジェクト: @layer で構造的に設計
  • 理想: @layer で大枠を決め、:where() で細かく調整

@layer については、改めてしっかり勉強して別の記事として詳しく解説する予定です!

貴重なフィードバックをくださった読者の方、本当にありがとうございました!🙏


📚 関連記事

この記事で触れた @layer について、詳しく解説した続編を公開しました!

CSS @layerでリセットCSSの上書き問題を解決!シンプルさこそ最強な理由

  • なぜ @layer がリセットCSSに最適なのか
  • Cascade Layersの仕組みを詳しく解説
  • 複雑なレイヤー構造のアンチパターンと注意点
  • 実際に動くCodePenデモ付き

ぜひ合わせてご覧ください!✨

Discussion

junerjuner

リセット css 云々なら 多分 @layer 使った方がよさそう……?

/* メイン */
body {
   background: yellow;
   /* こっちが勝つ */
}

@layer base {
  /* リセット css */
  body {
    background: red;
  }
}

https://developer.mozilla.org/ja/docs/Web/CSS/Reference/At-rules/@layer

sakusaku

コメントありがとうございます!

@layer のご指摘、まさにその通りですね!正直なところ、Cascade Layers についてちゃんと理解できていませんでした。不勉強ですみません🙇

調べてみたら、確かにリセットCSSには :where() よりも @layer の方が優秀ですね。詳細度とは独立した優先順位の層を作れるので、より明確に設計できることがわかりました。

この記事にも追記させていただきます。そして @layer については改めてしっかり勉強して、次の記事として詳しく書きたいと思います!

貴重なフィードバック、ありがとうございました!