:nth-child() の "of S" 構文が最強な件について

# はじめに

この記事は [2023 年の MDN 翻訳 Advent Calendar](https://qiita.com/advent-calendar/2023/mdn-translation-community-japan) 向けに作成したものです。

こんにちは。debiru です。[CSS/DHTMLバグ辞典スレッド](http://web.archive.org/web/20081015035607/http://cssbug.at.infoseek.co.jp/)の住人です。

さて、今日は、ネタがないので MDN っていうよりもただの CSS のお話をしたいと思います。

# 2023 年になって使えるようになった CSS テクニック

Chrome for Developers というサイトでは次のような記事があります。

- [CSS Wrapped: 2023 年  |  Blog  |  Chrome for Developers](https://developer.chrome.com/blog/css-wrapped-2023?hl=ja)
- [CSS と UI の新機能: I/O 2023 Edition  |  Blog  |  Chrome for Developers](https://developer.chrome.com/blog/whats-new-css-ui-2023?hl=ja)

今日はこの中から、みんな大好き `:nth-child()` 疑似クラスの話をしてみます。

## `:nth-child()` の基本的な使い方

次のような HTML があるとします。

```html
<section>
  <h1 class="item note">[1]</h1>
  <p class="item">[2]</p>
  <p class="item note">[3]</p>
  <p class="item">[4]</p>
  <p class="item note">[5]</p>
  <h1 class="item">[6]</h1>
  <p class="item">[7]</p>
  <p class="item">[8]</p>
  <p class="item">[9]</p>
  <p class="item">[10]</p>
</section>
```

これに次のような CSS セレクタを書くと、それぞれ以下に示す番号の要素にマッチします。

```css
section > :nth-child(1) { /* 先頭の要素 */ }
/* [1] */
/* index ではなく nth なので最初の要素は 1 で表されます */

section > :nth-child(n) { /* すべての要素 */ }
/* [1], [2], [3], [4], [5], [6], [7], [8], [9], [10] */
/* n の値は 0, 1, 2, 3, ... のように振る舞います */

section > :nth-child(n + 3) { /* 3個目以降の要素 */ }
/* [3], [4], [5], [6], [7], [8], [9], [10] */

section > :nth-child(-n + 3) { /* 3個目以前の要素 */ }
/* [1], [2], [3] */

section > :nth-child(2n) { /* 2の倍数の要素 */ }
/* [2], [4], [6], [8], [10] */

section > :nth-child(2n + 1) { /* 奇数の要素 */ }
/* [1], [3], [5], [7], [9] */

section > :nth-child(even) { /* 偶数の要素 */ }
/* [2], [4], [6], [8], [10] */

section > :nth-child(odd) { /* 奇数の要素 */ }
/* [1], [3], [5], [7], [9] */
```

こんな感じで、「何個目にある要素」という指定ができます。

## `:nth-child()` と `:nth-of-type()`

```html
<section>
  <h1 class="item note">[1]</h1>
  <p class="item">[2]</p>
  <p class="item note">[3]</p>
  <p class="item">[4]</p>
  <p class="item note">[5]</p>
  <h1 class="item">[6]</h1>
  <p class="item">[7]</p>
  <p class="item">[8]</p>
  <p class="item">[9]</p>
  <p class="item">[10]</p>
</section>
```

```css
section > p:nth-child(1) { /* 該当しません */ }

section > p:nth-of-type(1) { /* 最初の p 要素 */ }
/* [2] */

section > .item:nth-child(2) { /* 2番目の要素が .item にマッチする場合 */ }
/* [2] */
/* 注意：2番目の .item の要素ではありません */

section > .item:nth-of-type(2) { /* 要素名ごとに2番目の要素が .item にマッチする場合 */ }
/* 2個目の p = [3], そして2個目の h1 = [6] */

section > .note:nth-child(2) { /* 該当しません */ }
/* なぜなら [2] は .note を持たないからです */

section > .note:nth-of-type(2) { /* 要素名ごとに2番目の要素が .note にマッチする場合 */ }
/* [3] */
```

## 思い出話

どうでしょうか。2008 年頃に `:nth-child()`, `:nth-of-type()` が各種ブラウザでサポートされ始めたとき、ワクワクすると同時に、本当に欲しいのはこれじゃない感がなかったでしょうか。

![nth-child は Chrome 1, Firefox 3.5, Safari 3.1 などでサポートされているが Edge は 12 からサポートされた。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178936/c228fca0-f976-80d1-aade-1da13b421d17.png)

このブラウザ互換性テーブルの表を見てください。各種ブラウザで 2008 年頃にサポートされたことが分かる小さな値（初期頃のバージョン）が並んでいますが、これがサポートされていない時期があったのです。しかもですよ、IE では 11 ですらサポートされておらず、上記のブラウザでサポートされていても IE が生きていた 2020 年頃まではこれらの機能を満足に使えなかった（それこそ当時の [Baseline](https://qiita.com/debiru/items/1f4bfdfb5bbb0b9c5a3a) ではなかった）のです！

そうです。いまこの記事をお読みの IE6 対応に悩まされ続けたあなたの心に問いかけています。IE6 全盛期の 2005 年前後には当然ながらこんな機能は使えず、IE, Edge を無視する形で 2008 年から使えるようになったのがこれらの時代の機能なのです。

ブラウザの登場と歴史の話については、私の資料「[Webの誕生とブラウザの歴史](https://lavoscore.org/assets/file/history-of-web-and-browser.pdf)」を読んで感傷に浸ってください。

そんな、2008 年頃にようやく登場した `:nth-child()` と `:nth-of-type()` でしたが、先程の説明をよく見てください。**`/* 注意：2番目の .item の要素ではありません */`** と書いたように、これを実現するセレクタは書けませんでした。

## 2023 年なら書ける：進化した `:nth-child()`

```html
<section>
  <h1 class="item note">[1]</h1>
  <p class="item">[2]</p>
  <p class="item note">[3]</p>
  <p class="item">[4]</p>
  <p class="item note">[5]</p>
  <h1 class="item">[6]</h1>
  <p class="item">[7]</p>
  <p class="item">[8]</p>
  <p class="item">[9]</p>
  <p class="item">[10]</p>
</section>
```

### クイズです：3個目の `.note` にマッチするセレクタを書いてください

これは次のように書けます。

```css
section > :nth-child(3 of .note) { /* 3個目の .note にマッチする要素 */ }
/* [5] */

/** 構文
 * :nth-child(An+B [of S]?)
 * "of S" 構文を用いることで、任意のセレクタに対する順序指定ができます。
 * S はセレクタリストです。複雑なセレクタを指定することもできます。
 */
```

- https://drafts.csswg.org/selectors/#the-nth-child-pseudo

`of S` 構文は、Chrome, Edge, Opera, Firefox では 2023 年 2 月から 5 月にかけて実装されました。ちなみになんと Safari では 2015 年から実装されているようです。

### `of S` 構文の応用：フィルタリング可能なゼブラストライプの実現

```html
<table id="table">
  <thead>
    <tr><th>Name</th><th>Age</th><th>Country</th></tr>
  </thead>
  <tbody>
    <tr><td>Mamitiana</td><td>23</td><td>Madagascar</td></tr>
    <tr><td>Yuki</td><td>48</td><td>Japan</td></tr>
    <tr><td>Tlayolotl</td><td>36</td><td>Mexico</td></tr>
    <tr><td>Adilah</td><td>27</td><td>Morocco</td></tr>
    <tr><td>Vieno</td><td>55</td><td>Finland</td></tr>
    <tr><td>Ricardo</td><td>66</td><td>Brazil</td></tr>
  </tbody>
</table>
```

```css
#table tbody tr:nth-child(odd) { background: gray; }
#table tbody tr:nth-child(even) { background: white; }
```

|Name|Age|Country|nth|color|
|:---:|:---:|:---:|:---:|:---:|
|Mamitiana|23|Madagascar|1|gray|
|Yuki|48|Japan|2|white|
|Tlayolotl|36|Mexico|3|gray|
|Adilah|27|Morocco|4|white|
|Vieno|55|Finland|5|gray|
|Ricardo|66|Brazil|6|white|

このようなテーブルがあったとして、例えば年齢が30代の人を非表示にするというフィルタリングを行ったとしましょう。JavaScript で `hidden` 属性を `tr` 要素に与えたり外したりするものとします。

フィルタリングを実行すると、次のような HTML になります。

```html
<table id="table">
  <thead>
    <tr><th>Name</th><th>Age</th><th>Country</th></tr>
  </thead>
  <tbody>
    <tr><td>Mamitiana</td><td>23</td><td>Madagascar</td></tr>
    <tr><td>Yuki</td><td>48</td><td>Japan</td></tr>
    <tr hidden><td>Tlayolotl</td><td>36</td><td>Mexico</td></tr>
    <tr><td>Adilah</td><td>27</td><td>Morocco</td></tr>
    <tr><td>Vieno</td><td>55</td><td>Finland</td></tr>
    <tr><td>Ricardo</td><td>66</td><td>Brazil</td></tr>
  </tbody>
</table>
```

|Name|Age|Country|nth|color|
|:---:|:---:|:---:|:---:|:---:|
|Mamitiana|23|Madagascar|1|gray|
|Yuki|48|Japan|2|white|
|Adilah|27|Morocco|4|white|
|Vieno|55|Finland|5|gray|
|Ricardo|66|Brazil|6|white|

Qiita のテーブルでは `hidden` ではなく本当に要素がないのでゼブラストライプがうまくいっていますが、実際には表で示した通り、`nth = 3` の `tr` がいなくなって、`2` と `4` が連続することにより、ストライプが破綻してしまいます。

これを防ぐためには `of S` 構文が使えます。

```css
#table tbody tr:nth-child(odd of :not([hidden]) { background: gray; }
#table tbody tr:nth-child(even of :not([hidden]) { background: white; }
```

|Name|Age|Country|nth|color|
|:---:|:---:|:---:|:---:|:---:|
|Mamitiana|23|Madagascar|1|gray|
|Yuki|48|Japan|2|white|
|Adilah|27|Morocco|3|gray|
|Vieno|55|Finland|4|white|
|Ricardo|66|Brazil|5|gray|

`hidden` の要素を無視して `nth` が計算されるので、期待通りのゼブラストライプが実現できます。

- https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child#using_of_selector_to_fix_striped_tables

# さいごに

知っていた方にはつまらない話題だったかもしれませんが、ネタがないので単なる CSS のお話をしてしまいました。MDN に関するネタ、MDN Community で誰からもお返事がいただけないので記事にできないかもしれません。残念です。

というか [2023 年の MDN 翻訳 Advent Calendar](https://qiita.com/advent-calendar/2023/mdn-translation-community-japan) で私が書いたこれまでの記事も、Qiita のコメントも Twitter での言及もなく寂しいので IE6 対応していたインターネット老人会のみなさんがもしこの記事を読んだら何かしら反応いただけると嬉しいです。

このアドベントカレンダー、頑張って書いてるけど読んでくれてる人いるのかな……。

というわけで、`:nth-child()` が最強で `:nth-of-type()` はいらない子というお話でした。

おわり。