詳細度は合計じゃない!階層で理解する:where() vs :is()
はじめに:詳細度との戦い
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
リセット css 云々なら 多分
@layer使った方がよさそう……?コメントありがとうございます!
@layer のご指摘、まさにその通りですね!正直なところ、Cascade Layers についてちゃんと理解できていませんでした。不勉強ですみません🙇
調べてみたら、確かにリセットCSSには :where() よりも @layer の方が優秀ですね。詳細度とは独立した優先順位の層を作れるので、より明確に設計できることがわかりました。
この記事にも追記させていただきます。そして @layer については改めてしっかり勉強して、次の記事として詳しく書きたいと思います!
貴重なフィードバック、ありがとうございました!