はじめに#
CSSセレクタを書いていると、「同じスタイルを複数のセレクタに適用したいが、記述が冗長になる」「親要素を子要素の状態に応じてスタイリングしたい」といった課題に直面することがあります。従来のCSSでは、これらの課題を解決するために複雑なセレクタリストや追加のクラス、JavaScriptによる制御が必要でした。
CSS Selectors Level 4で追加された:is()、:where()、:has()、および強化された:not()は、これらの課題を解決し、より効率的でシンプルなCSSの記述を可能にします。
本記事では、以下の内容を解説します。
:is()による効率的なセレクタのグルーピング
:where()による詳細度ゼロのセレクタ記述
:has()による親要素・兄弟要素の選択
:not()による否定セレクタの活用
- 各擬似クラスが詳細度に与える影響
- 実践的なユースケースとベストプラクティス
前提条件#
本記事を読み進めるにあたり、以下の知識があることを前提としています。
- CSSの基本構文(セレクタ・プロパティ・値の関係)
- 基本的なCSSセレクタ(要素・クラス・ID・属性セレクタ)
- CSSの詳細度(Specificity)の基本概念
動作確認環境#
本記事で紹介するすべての擬似クラスは、2024年以降の主要ブラウザで完全にサポートされています。
| 擬似クラス |
Chrome |
Firefox |
Safari |
Edge |
:is() |
88+ |
78+ |
14+ |
88+ |
:where() |
88+ |
78+ |
14+ |
88+ |
:has() |
105+ |
121+ |
15.4+ |
105+ |
:not() セレクタリスト対応 |
88+ |
84+ |
9+ |
88+ |
:is() - セレクタリストのグルーピング#
:is()擬似クラスは、セレクタリストを引数として受け取り、そのリスト内のいずれかのセレクタにマッチする要素を選択します。複数のセレクタに同じスタイルを適用する際の記述を大幅に簡略化できます。
基本構文#
1
2
3
|
:is(<セレクタリスト>) {
/* スタイル */
}
|
従来の記述との比較#
従来のCSSでは、ネストした要素に対して同じスタイルを適用する場合、すべての組み合わせを列挙する必要がありました。
従来の記述方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/* section, article, aside, nav配下のh1に異なるフォントサイズを適用 */
section h1,
article h1,
aside h1,
nav h1 {
font-size: 1.5rem;
}
/* 2階層ネストの場合はさらに組み合わせが増加 */
section section h1,
section article h1,
section aside h1,
section nav h1,
article section h1,
article article h1,
article aside h1,
article nav h1,
aside section h1,
aside article h1,
aside aside h1,
aside nav h1,
nav section h1,
nav article h1,
nav aside h1,
nav nav h1 {
font-size: 1.25rem;
}
|
:is()を使用した記述:
1
2
3
4
5
6
7
8
9
|
/* 1階層ネスト */
:is(section, article, aside, nav) h1 {
font-size: 1.5rem;
}
/* 2階層ネスト */
:is(section, article, aside, nav) :is(section, article, aside, nav) h1 {
font-size: 1.25rem;
}
|
:is()を使用することで、16行必要だったセレクタが1行で記述できるようになります。
実践的な使用例#
ナビゲーションリンクのスタイリング#
1
2
3
4
5
6
7
8
9
10
|
/* ヘッダー・フッター・サイドバー内のナビゲーションリンク */
:is(header, footer, .sidebar) nav a {
color: #333;
text-decoration: none;
}
:is(header, footer, .sidebar) nav a:hover {
color: #0066cc;
text-decoration: underline;
}
|
フォーム要素のスタイリング#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* テキスト系入力フィールド */
:is(input[type="text"], input[type="email"], input[type="password"], textarea) {
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
:is(input[type="text"], input[type="email"], input[type="password"], textarea):focus {
border-color: #0066cc;
outline: none;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
|
寛容なセレクタパース(Forgiving Selector Parsing)#
:is()の重要な特徴として、「寛容なセレクタパース」があります。セレクタリスト内に無効なセレクタが含まれていても、有効なセレクタのみが適用され、ルール全体が無効になることはありません。
1
2
3
4
|
/* :unsupportedが無効でも、.validは正常に適用される */
:is(.valid, :unsupported) {
color: blue;
}
|
従来のセレクタリストでは、1つでも無効なセレクタがあるとルール全体が無視されていたため、これは大きな改善です。
1
2
3
4
5
|
/* 従来の記述::unsupportedが無効だとルール全体が無視される */
.valid,
:unsupported {
color: blue; /* 適用されない可能性がある */
}
|
:where() - 詳細度ゼロのセレクタグルーピング#
:where()擬似クラスは、:is()と同じくセレクタリストを引数として受け取りますが、決定的な違いがあります。:where()自体の詳細度は常に0です。
基本構文#
1
2
3
|
:where(<セレクタリスト>) {
/* スタイル */
}
|
:is()と:where()の詳細度の違い#
:is()と:where()の最も重要な違いは、詳細度への影響です。
graph LR
subgraph ":is()の詳細度"
A[":is(#id, .class, p)"] --> B["引数内で最も高い詳細度を採用<br/>= 1-0-0(IDの詳細度)"]
end
subgraph ":where()の詳細度"
C[":where(#id, .class, p)"] --> D["常に詳細度 0-0-0"]
end詳細度の比較例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* :is()の詳細度 = 引数内で最も高いセレクタの詳細度 */
:is(#header, .nav, p) a {
color: red;
}
/* 詳細度: 1-0-1(ID + 要素) */
/* :where()の詳細度 = 常に0 */
:where(#header, .nav, p) a {
color: blue;
}
/* 詳細度: 0-0-1(要素のみ) */
/* 要素セレクタだけでも上書き可能 */
footer a {
color: green;
}
/* 詳細度: 0-0-2(要素 + 要素) */
|
上記の例では、:where()で指定したスタイルは、より低い詳細度を持つため、footer aのルールで簡単に上書きできます。
:where()の活用シーン#
:where()は、以下のようなシーンで特に有効です。
CSSライブラリやリセットCSS#
ライブラリのデフォルトスタイルを簡単に上書きできるようにする場合に適しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/* リセットCSS / ベーススタイル */
:where(h1, h2, h3, h4, h5, h6) {
margin: 0;
font-weight: normal;
}
:where(ul, ol) {
padding-left: 0;
list-style: none;
}
/* ユーザーは単純なセレクタで上書き可能 */
h1 {
font-size: 2rem;
font-weight: bold;
}
|
ユーティリティクラスの定義#
詳細度の競合を避けたいユーティリティクラスに適しています。
1
2
3
4
5
6
7
8
9
|
/* 詳細度0のユーティリティ */
:where(.mt-4) { margin-top: 1rem; }
:where(.mb-4) { margin-bottom: 1rem; }
:where(.text-center) { text-align: center; }
/* コンポーネント側で簡単に上書き可能 */
.card {
margin-top: 2rem; /* .mt-4を適用していても上書きできる */
}
|
:is()と:where()の使い分け#
| 観点 |
:is() |
:where() |
| 詳細度 |
引数内で最も高いセレクタの詳細度を継承 |
常に0 |
| 用途 |
通常のスタイル定義、セレクタの簡略化 |
ベーススタイル、ライブラリ、上書きを想定したスタイル |
| 上書きのしやすさ |
通常の詳細度ルールに従う |
簡単に上書き可能 |
:has() - 親要素・兄弟要素セレクタ#
:has()擬似クラスは、CSS史上最も革新的な機能の1つです。従来、CSSでは「子要素の状態に応じて親要素をスタイリングする」ことは不可能でした。:has()はこの制限を打ち破り、親セレクタ(Parent Selector)とも呼ばれる機能を実現します。
基本構文#
1
2
3
|
要素:has(<相対セレクタリスト>) {
/* スタイル */
}
|
:has()は、引数に指定した相対セレクタにマッチする要素を含む(または隣接する)場合に、アンカー要素を選択します。
親要素の選択#
特定の子要素を持つ親要素を選択できます。
1
2
3
4
5
|
/* .featuredクラスを持つ子要素を含むsection */
section:has(.featured) {
border: 2px solid #0066cc;
background-color: #f0f8ff;
}
|
1
2
3
4
5
6
7
8
9
10
|
<section>
<article class="featured">注目コンテンツ</article>
<article>通常コンテンツ</article>
</section>
<!-- ↑ このsectionにスタイルが適用される -->
<section>
<article>通常コンテンツ</article>
</section>
<!-- ↑ このsectionにはスタイルが適用されない -->
|
直接の子要素を持つ親の選択#
子結合子(>)を使用して、直接の子要素に限定できます。
1
2
3
4
5
6
7
8
9
10
11
|
/* 直接の子として画像を持つfigure */
figure:has(> img) {
display: flex;
justify-content: center;
}
/* 直接の子としてfigcaptionを持つfigure */
figure:has(> figcaption) {
flex-direction: column;
align-items: center;
}
|
兄弟要素による選択#
隣接兄弟結合子(+)や一般兄弟結合子(~)を使用して、後続の兄弟要素に基づいて選択できます。
1
2
3
4
5
6
7
8
9
|
/* 直後にh2が続くh1(見出しグループの先頭) */
h1:has(+ h2) {
margin-bottom: 0.5rem; /* 通常より狭いマージン */
}
/* 後続の兄弟にエラーメッセージがあるinput */
input:has(~ .error-message) {
border-color: #dc3545;
}
|
実践的なユースケース#
フォームバリデーションのスタイリング#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* 必須フィールドを含むフォームグループ */
.form-group:has(input:required) label::after {
content: " *";
color: #dc3545;
}
/* 無効な入力があるフォームグループ */
.form-group:has(input:invalid) {
border-left: 3px solid #dc3545;
padding-left: 1rem;
}
/* 有効な入力があるフォームグループ */
.form-group:has(input:valid) {
border-left: 3px solid #28a745;
padding-left: 1rem;
}
|
カードコンポーネントの条件付きスタイリング#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/* 画像を含むカード */
.card:has(img) {
padding: 0;
}
.card:has(img) .card-body {
padding: 1.5rem;
}
/* 画像を含まないカード */
.card:not(:has(img)) {
padding: 1.5rem;
}
/* フッターを持つカード */
.card:has(.card-footer) .card-body {
border-bottom: 1px solid #eee;
}
|
ダークモードの条件付き切り替え#
1
2
3
4
5
6
7
8
9
|
/* ダークモードのトグルがチェックされている場合 */
body:has(#dark-mode-toggle:checked) {
--bg-color: #1a1a2e;
--text-color: #eee;
--accent-color: #4da8da;
background-color: var(--bg-color);
color: var(--text-color);
}
|
:has()の論理演算#
:has()はカンマ区切りでOR条件、連結でAND条件を表現できます。
1
2
3
4
5
6
7
8
9
|
/* OR条件:videoまたはaudioを含む要素 */
.media-container:has(video, audio) {
background-color: #000;
}
/* AND条件:videoとaudioの両方を含む要素 */
.media-container:has(video):has(audio) {
background-color: #1a1a1a;
}
|
パフォーマンスに関する注意点#
:has()は強力ですが、パフォーマンスに影響を与える可能性があります。以下のベストプラクティスを守ることを推奨します。
- アンカー要素を限定する:
body:has()や*:has()のような広範なセレクタは避ける
- 子結合子を使用する: 可能な限り
>や+で探索範囲を制限する
- 内部セレクタを具体的にする:
.ancestor:has(.foo)より.ancestor:has(> .foo)の方が効率的
1
2
3
4
5
6
7
|
/* 避けるべきパターン */
body:has(.sidebar-expanded) { /* ... */ }
*:has(.item) { /* ... */ }
/* 推奨パターン */
.container:has(> .sidebar-expanded) { /* ... */ }
.gallery:has(> img[data-loaded="false"]) { /* ... */ }
|
:not() - 否定擬似クラス#
:not()擬似クラスは、引数に指定したセレクタにマッチしない要素を選択します。CSS Selectors Level 4では、複数のセレクタをカンマ区切りで指定できるようになりました。
基本構文#
1
2
3
|
:not(<セレクタリスト>) {
/* スタイル */
}
|
単一セレクタの否定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/* .fancyクラスを持たないp要素 */
p:not(.fancy) {
color: #333;
}
/* disabled属性を持たないbutton */
button:not([disabled]) {
cursor: pointer;
}
/* 最後の子要素以外のli */
li:not(:last-child) {
border-bottom: 1px solid #eee;
}
|
複数セレクタの否定(CSS Selectors Level 4)#
1
2
3
4
5
6
7
8
9
|
/* .fooと.barのどちらのクラスも持たないdiv */
div:not(.foo, .bar) {
background-color: #f5f5f5;
}
/* 上記は以下と同等 */
div:not(.foo):not(.bar) {
background-color: #f5f5f5;
}
|
実践的な使用例#
ナビゲーションの区切り線#
1
2
3
4
5
6
|
/* 最後の要素以外に右ボーダーを適用 */
.nav-item:not(:last-child) {
border-right: 1px solid #ddd;
padding-right: 1rem;
margin-right: 1rem;
}
|
フォーム要素の一括スタイリング#
1
2
3
4
5
6
|
/* submit/reset以外のinput */
input:not([type="submit"], [type="reset"], [type="button"]) {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
}
|
特定クラスを除外したスタイリング#
1
2
3
4
5
6
7
8
9
10
|
/* .no-styleクラスを持たないすべてのリンク */
a:not(.no-style) {
color: #0066cc;
text-decoration: underline;
}
/* ヘッダー内以外のすべてのh1 */
h1:not(header h1) {
margin-top: 2rem;
}
|
:not()の注意点#
:not()を使用する際は、以下の点に注意が必要です。
- 意図しない要素への適用:
:not(.foo)は、<html>や<body>を含むすべての.fooでない要素にマッチします
1
2
3
4
5
6
7
8
9
|
/* 注意:これはhtml, bodyにも適用される */
:not(.special) {
margin: 0;
}
/* 改善:対象要素を限定する */
p:not(.special) {
margin: 0;
}
|
- 子孫セレクタとの組み合わせ: 意図しない経路でマッチする可能性があります
1
2
3
4
5
|
/* 注意:table内のリンクにも適用される可能性がある */
/* (tr, td, tbodyなどはtableではないため) */
body :not(table) a {
color: blue;
}
|
- 無効なセレクタによるルール無効化:
:not()内に無効なセレクタがあるとルール全体が無効になります
1
2
3
4
5
6
7
8
9
|
/* 無効:ルール全体が適用されない */
p:not(.foo, :invalid-pseudo-class) {
color: red;
}
/* 改善::is()でラップして寛容なパースを利用 */
p:not(:is(.foo, :invalid-pseudo-class)) {
color: red;
}
|
詳細度への影響まとめ#
各擬似クラスが詳細度に与える影響をまとめます。
graph TD
subgraph 詳細度の計算
A[":is()"] --> A1["引数内で最も高い詳細度を採用"]
B[":where()"] --> B1["常に 0-0-0"]
C[":has()"] --> C1["引数内で最も高い詳細度を採用<br/>(:is()と同様)"]
D[":not()"] --> D1["引数内で最も高い詳細度を採用<br/>(:is()と同様)"]
end詳細度計算の具体例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/* 詳細度の比較 */
/* 0-1-1: クラス(1) + 要素(1) */
:is(.nav, .sidebar) a { }
/* 0-0-1: 要素(1)のみ(:where自体は0) */
:where(.nav, .sidebar) a { }
/* 1-0-1: ID(1) + 要素(1) */
section:has(#featured) { }
/* 0-1-1: クラス(1) + 要素(1) */
p:not(.intro) { }
/* 1-0-0: ID(1)のみ(#fooが最も高い) */
:not(#foo, .bar) { }
|
詳細度を活用した設計パターン#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/* ベーススタイル(上書きしやすい) */
:where(button) {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* テーマスタイル(:is()で適切な詳細度を確保) */
:is(.btn-primary) {
background-color: #0066cc;
color: white;
}
:is(.btn-secondary) {
background-color: #6c757d;
color: white;
}
/* 状態スタイル(:has()で状態に応じた親のスタイリング) */
.form-group:has(:invalid) {
--border-color: #dc3545;
}
/* 除外スタイル(:not()で特定要素を除外) */
.btn-primary:not([disabled]) {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
|
組み合わせパターン#
これらの擬似クラスを組み合わせることで、より表現力豊かなセレクタを構築できます。
:is()と:has()の組み合わせ#
1
2
3
4
|
/* h2またはh3が後続するh1, h2, h3 */
:is(h1, h2, h3):has(+ :is(h2, h3, h4)) {
margin-bottom: 0.5rem;
}
|
:where()と:not()の組み合わせ#
1
2
3
4
5
6
|
/* リセットスタイル(上書きしやすい) */
:where(ul, ol):not(.styled-list) {
list-style: none;
padding: 0;
margin: 0;
}
|
:has()と:not()の組み合わせ#
1
2
3
4
5
6
7
8
9
10
11
12
|
/* 画像を含まないカード */
.card:not(:has(img)) {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
/* エラーがない有効なフォームグループ */
.form-group:has(input:valid):not(:has(.error)) {
border-left-color: #28a745;
}
|
複合パターン例:レスポンシブナビゲーション#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
/* メニュー項目を持つナビゲーション */
nav:has(> ul > li) {
display: flex;
gap: 1rem;
}
/* サブメニューを持つ項目 */
nav li:has(> ul) {
position: relative;
}
nav li:has(> ul) > a::after {
content: " ▼";
font-size: 0.75em;
}
/* サブメニュー自体 */
nav li:has(> ul) > ul {
display: none;
position: absolute;
top: 100%;
left: 0;
}
/* ホバー時にサブメニューを表示 */
nav li:has(> ul):hover > ul {
display: block;
}
/* リンク以外の項目(区切り線など) */
nav li:not(:has(a)) {
border-right: 1px solid #ddd;
padding-right: 1rem;
}
|
まとめ#
モダンCSSセレクタである:is()、:where()、:has()、:not()は、CSSの表現力と保守性を大幅に向上させます。
| 擬似クラス |
主な用途 |
詳細度への影響 |
:is() |
セレクタのグルーピング、記述の簡略化 |
引数内で最も高い詳細度を継承 |
:where() |
ベーススタイル、上書きを想定したスタイル |
常に0 |
:has() |
親要素の選択、状態に応じたスタイリング |
引数内で最も高い詳細度を継承 |
:not() |
特定要素の除外 |
引数内で最も高い詳細度を継承 |
これらの擬似クラスを活用することで、以下のメリットが得られます。
- 記述の簡略化: 冗長なセレクタリストを削減
- 詳細度の制御:
:where()による柔軟な詳細度管理
- 新しい選択パターン:
:has()による親要素選択の実現
- 保守性の向上: シンプルで理解しやすいセレクタ
ただし、特に:has()については、パフォーマンスへの影響を考慮し、アンカー要素と内部セレクタを適切に限定することが重要です。
参考リンク#