CSSの擬似クラスとJavaScript対応表

様々な擬似クラスの登場により、それぞれの用途に応じて柔軟に対象を絞り込んでスタイルを適用することが容易になりました。
これまでJavaScriptで実装していた機能の多くが、CSSでも実現可能になった部分も増えています。

しかし、依然としてJavaScriptで操作した方が見通しが良かったり、CSSだけではどうしても対応できないケースもあるので、それらについてまとめてみます。

```html:ベースとなるHTML
<div class="container">
    <ul>
        <li class="item">A (li.item)</li>
        <li class="item">B (li.item)</li>
        <li class="item">C (li.item)</li>
        <li>D (li)</li>
    </ul>
    <p>E (p)</p>
    <p class="item">F (p.item)</p>
    <ul>
        <li class="item">G (li.item)</li>
        <li>H (li)</li>
        <li class="item">I (li.item)</li>
        <li class="item">J (li.item)</li>
    </ul>
    <ul>
        <li>K (li)</li>
        <li>L (li)</li>
        <li>M (li)</li>
    </ul>
    <ul>
        <li class="item">N (li.item)</li>
        <li class="item">O (li.item)</li>
    </ul>
    <p>P (p)</p>
    <p class="item">Q (p.item)</p>
    <ul>
        <li>R (li)</li>
        <li class="item">S (li.item)</li>
        <li>T (li)</li>
    </ul>
</div>
```

```css:ベースとなるCSS
.container {
    border: 1px solid black;
    font-family: monospace;
    width: 200px;
}
```

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="ZYzbWxW" data-pen-title="Untitled" data-user="yoruaki" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yoruaki/pen/ZYzbWxW">
  Untitled</a> by Yoruaki (<a href="https://codepen.io/yoruaki">@yoruaki</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

:::note
コードの簡潔さと可読性を考慮して、便宜上`.container`で全体を囲んでいます。

そのため、`.container`の兄弟要素にはスタイルが適用されないなどの制約がありますが、本記事では擬似クラスの適用範囲や動作確認を視覚的に分かりやすくすることを目的としています。
:::

## 全ての`.item`にスタイルを適用したい

```css:CSS
.container .item {
    background-color: red;
}
```

```js:JavaScript
document.querySelectorAll('.item').forEach(function(element) {
    element.style.backgroundColor = 'red';
});
```

CSSで対応できるものは、CSSに任せましょう。

## 最初の`.item`にスタイルを適用したい

```css:CSS
.container .item:first-of-type {
    background-color: red;
}
```

このセレクタは、各親要素の中で最初に出現する要素かつ`.item`だった場合に適用されるため、複数の親要素がある場合や、最初に`.item`が来ない場合には意図した結果にならないことがあります。
つまり「最初の要素ではないかつ最初に出現する`.item`」には適用されません。

今回のケースだとA、G、Nに適用され、FとSには適用されません。

![`.item:first-of-type`の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/17088b56-298b-d898-f73e-ee090e7f5109.png)


最初の要素に`.item`が出現することが確定しているなら、そのままCSSで指定しましょう。

### 各要素内で最初に出現する`.item`にスタイルを適用したい

CSSでは「最初の要素」は取得できても、「最初に出現する特定の要素」は取得できません。
そのため、この場合はJavaScriptでの実装が必要です。

```js:JavaScript
document.querySelectorAll('.container > *').forEach(parent => {
    const firstItem = parent.querySelector('.item');
    if (firstItem) {
        firstItem.style.backgroundColor = 'red';
    }
    if (parent.classList.contains('item')) {
        parent.style.backgroundColor = 'red';
    }
});
```

![各要素内で最初に出現する`.item`にスタイルを適用した例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/5bdeac2f-a6b3-ed25-1fe3-70f8b3f21baa.png)


### ドキュメント全体で最初に出現する`.item`だけにスタイルを適用したい

あまりないとは思いますが、ドキュメント全体で最初の`.item`だけにスタイルを適用したい場合は、JavaScriptで実装するしかありません。

```js:JavaScript
const firstItem = document.querySelector('.item');
if (firstItem) {
    firstItem.style.backgroundColor = 'red';
}
```

![ドキュメント全体で最初に出現する`.item`だけにスタイルを適用した例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/08ecec5f-dcd1-5bb0-e46e-237a63447aad.png)

## 最後の`.item`にスタイルを適用したい


```css:CSS
.container .item:last-of-type {
    background-color: red;
}
```

こちらも`.item:first-of-type`セレクタと同じく、「最後の要素ではないかつ最後に出現する`.item`」には適用されません。

![`.item:last-of-type`の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/f35b0fec-0323-7510-50ba-72174f10cbc9.png)

このケースも、本来の意図はC、Sにも適用させたいものと想像できます。
もしこのままでOKなら、そのままCSSで指定しましょう。

### 各要素内で最後に出現する`.item`にスタイルを適用したい

こちらも同じく、CSSでは「最後の要素」は取得できても、「最後に出現する特定の要素」は取得できません。
そのため、この場合もJavaScriptが必要です。

```js:JavaScript
document.querySelectorAll('.container > *').forEach(parent => {
    const childItems = parent.querySelectorAll('.item');
    if (childItems.length > 0) {
        const lastItem = childItems[childItems.length - 1];
        lastItem.style.backgroundColor = 'red';
    }
});

const parentItems = document.querySelectorAll('.container > .item');
const lastParentItem = parentItems[parentItems.length - 1];
if (lastParentItem) {
    lastParentItem.style.backgroundColor = 'red';
}
```

![各要素内で最後に出現する`.item`にスタイルを適用した例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/c75bc206-3372-9927-f67b-ae5f9d9862c2.png)

### ドキュメント全体で最後に出現する`.item`だけにスタイルを適用したい

こちらもあまりないとは思いますが、ドキュメント全体で最後の`.item`だけにスタイルを適用したい場合も、JavaScriptで実装するしかありません。

```js:JavaScript
const items = document.querySelectorAll('.item');
if (items.length > 0) {
    const lastItem = items[items.length - 1];
    lastItem.style.backgroundColor = 'red';
}
```

![ドキュメント全体で最後に出現する`.item`だけにスタイルを適用した例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/568c9494-9bf9-9424-816c-887ef9059e1a.png)

## 特定の要素が`.item`だったらスタイルを適用したい

CSSで指定しましょう。

```css:CSS
.container p.item {
    background-color: red;
}
```

## `.item`を持っている親要素にスタイルを適用したい

CSSで指定しましょう。

```css:CSS
.container :has(.item) {
    background-color: red;
}
```

![`:has(.item)`の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/8e1f46dc-68bc-8f1c-0dfa-df4178c44c31.png)

## `.item`を持っていない親要素にスタイルを適用したい

```css:CSS
.container > *:not(:has(.item)) {
    background-color: red;
}
```

しかしこの場合、そもそも子要素を持っていない要素にもスタイルが適用されてしまいます。

![`*:not(:has(.item))`の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/86541209-f910-d98c-7e8c-9785ab9660be.png)

これが意図した通りならそのままCSSで指定しましょう。

CSSには、その要素が子要素を持っているかどうかを判定する機能はありません。その場合もJavaScriptで実装しましょう。

```js:JavaScript
document.querySelectorAll('.container > *').forEach(element => {
    if (element.children.length > 0 && !element.querySelector('.item')) {
        element.style.backgroundColor = 'red';
    }
});
```

![子要素がありかつ`.item`を持っている要素にスタイルを適用した例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45505/ccf430a0-04ee-fc48-80b9-6b64a15f9b47.png)

## 兄弟要素がない要素にスタイルを適用したい

以下のHTMLで、A、Dに適用したい場合ですね。

```html:HTML
<div class="container">
    <ul>
        <li>A</li>
    </ul>
    <ul>
        <li>B</li>
        <li>C</li>
    </ul>
    <ul>
        <li>D</li>
    </ul>
    <p>E</p>
</div>
```

CSSで指定しましょう。

```css:CSS
.container :only-child {
	  background-color: red;
}
```

 ### 同じ型で兄弟要素がない要素にスタイルを適用したい

 上記の例だと、A、D、Eに適用したい場合ですね。
 こちらもCSSで指定しましょう。

 ```css:CSS
.container :only-of-type {
	  background-color: red;
}
```

`only-child`でEに適用されなかった理由は、`p`要素は`ul`要素と型違いの兄弟だからです。
逆に`only-of-type`は、兄弟だけど型が違うからonlyだったのです。

## 兄弟要素の中で自分だけ`.item`を持っている要素にスタイルを適用したい

上記の例だと、Sに適用させたいパターンですね。
この場合、Sは`.item:only-child`でも`.item:only-of-type`でも指定できません。

ここはJavaScriptが適しています。

```js:JavaScript
document.querySelectorAll('.item').forEach(item => {
    const siblings = Array.from(item.parentElement.children);
    const itemSiblings = siblings.filter(sibling => sibling.classList.contains('item'));
    if (itemSiblings.length === 1) {
        item.style.backgroundColor = 'red';
    }
});
```

### 中身が空の`.item`にスタイルを適用したい

CSSで指定しましょう。

```css:CSS
.container .item:empty {
    background-color: red;
}
```

## 各セクションのまとめ

|ケース|CSSで対応可能|JavaScriptが必要|補足説明|
|---|---|---|---|
|全ての`.item`にスタイルを適用したい|✅|❌|CSSで十分|
|最初の`.item`にスタイルを適用したい|✅|❌|`:first-of-type`を使用|
|各要素内で最初に出現する`.item`にスタイルを適用したい|❌|✅|CSSでは難しい|
|ドキュメント全体で最初に出現する`.item`だけにスタイルを適用したい|❌|✅|CSSでは難しい|
|最後の`.item`にスタイルを適用したい|✅|❌|`:last-of-type`を使用|
|各要素内で最後に出現する`.item`にスタイルを適用したい|❌|✅|CSSでは難しい|
|ドキュメント全体で最後に出現する`.item`だけにスタイルを適用したい|❌|✅|CSSでは難しい|
|特定の要素が`.item`だったらスタイルを適用したい|✅|❌|セレクタで直接指定可能|
|`.item`を持っている親要素にスタイルを適用したい|✅|❌|`:has()`を使用|
|`.item`を持っていない親要素にスタイルを適用したい|✅|✅|`:not()`と`:has()`を使用<br>※子要素の有無も判定するならJSを使用|
|兄弟要素がない要素にスタイルを適用したい|✅|❌|`:only-child`を使用|
|同じ型で兄弟要素がない要素にスタイルを適用したい|✅|❌|`:only-of-type`を使用|
|兄弟要素の中で自分だけ`.item`を持っている要素にスタイルを適用したい|❌|✅|CSSでは難しい|
|中身が空の`.item`にスタイルを適用したい|✅|❌|`:empty`を使用|

## まとめ

以上、CSSの擬似クラスによるスタイル指定と、代替となるJavaScriptの使用例のまとめでした。

今回はHTMLが動的に変更され、こちらがコントロールできない場合を想定して書きましたが、通常、それ専用のクラスを適用したりすることで多くのケースをカバーできるかと思います。

しかしながら、中にはCSSだけではどうしても対応が難しい問題もあるかと思いますので、柔軟にCSSとJavaScriptを併用することで、より効果的なスタイリングをしていきましょう。

この記事が少しでもスタイル指定の参考になれば幸いです。