jQuery / Selector / :nth-child() - an+b 番目の兄弟要素を選択するセレクター

jQueryの :nth-child(an+b) は、各親の要素の兄弟を1から数え、式に合う位置を選びます。p:nth-child(2) は「2番目のp」ではなく「2番目の要素の子であり、かつp」を意味します。同じ親の2番目のpなら:nth-of-type(2)、選択集合全体の2番目なら.eq(1)と使い分けます。

1から数え、別のタグも含める

:nth-child() の公式文書はCSSの兄弟位置の規則に従います。親ごとに数え直し、異なるタグの要素も含めます。空白テキストとコメントは要素ではないため数えません。非表示でも文書構造に残る要素は位置を占めるので、見た目の順番だけでは判断できません。

h2、p、pの順ならp:nth-child(1)は空で、p:nth-child(2)が最初のpを選びます。pだけを集めてから番号を付ける処理ではありません。例のように > で直接の子に限定すると範囲が明確になります。

an+b とよく使う式

CSSの an+b の定義ではaとbは整数で、nには0、1、2などの非負整数を入れます。得られる正の位置だけが対象で、0や負の位置は選択しません。-n+3は3、2、1を作るため最初の3要素位置を意味し、末尾から数える負のインデックスではありません。

条件 要素位置 意味
2 2 各親の2番目の要素
2n または even 2, 4, 6, … 偶数位置
2n+1 または odd 1, 3, 5, … 奇数位置
3n+1 1, 4, 7, … 1から3つおき
-n+3 1, 2, 3 最初の3要素位置

前にpを付けると、その位置にあるpだけが残ります。4番目がspanならp:nth-child(2n)には含みませんが、spanを飛ばして後のpを4番目に数え直すこともしません。

タグが混在する完全な実行例

全コードをUTF-8の nth-child-demo.html に保存し、ブラウザーで開きます。インストールは不要ですが、CDN(Content Delivery Network、コンテンツ配信ネットワーク)から公式jQueryファイルを読む接続が必要です。コードは4.0.0を使い、式・追加・リセットと比較コードは3.7.1と4.0.0で実行確認しました。

Aの要素の子はh2、p、p、span、p、Bはh2、p、p、pです。結果にpのIDを表示するので、タグ位置と照合できます。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Sibling positions and formulas</title>
  <style>
    body { font: 16px/1.6 sans-serif; margin: 24px; }
    .group { border: 1px solid #aaa; padding: 12px; margin: 12px 0; }
    .group > * { display: block; margin: 6px 0; padding: 6px; }
    .group > .marked { background: #fff0b3; outline: 2px solid #b56b00; }
    button, select { padding: 8px; margin: 4px; }
  </style>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
</head>
<body>
  <h1>Sibling positions and formulas</h1>
  <div id="groups">
    <section class="group" id="group-a">
      <h2>A heading</h2>
      <p id="a1">A paragraph 1</p>
      <p id="a2">A paragraph 2</p>
      <span>A note</span>
      <p id="a3">A paragraph 3</p>
    </section>
    <section class="group" id="group-b">
      <h2>B heading</h2>
      <p id="b1">B paragraph 1</p>
      <p id="b2">B paragraph 2</p>
      <p id="b3">B paragraph 3</p>
    </section>
  </div>
  <label for="rule">Formula:</label>
  <select id="rule">
    <option value="2">2</option>
    <option value="2n">2n (even)</option>
    <option value="2n+1">2n+1 (odd)</option>
    <option value="3n+1">3n+1</option>
    <option value="-n+3">-n+3</option>
  </select>
  <button type="button" id="prepend">Prepend notice to A</button>
  <button type="button" id="reset">Reset</button>
  <output id="result" aria-live="polite"></output>
  <script>
    $(function () {
      function refresh() {
        $("#groups > .group > p").removeClass("marked");
        const rule = $("#rule").val();
        const $chosen = $("#groups > .group > p:nth-child(" + rule + ")");
        $chosen.addClass("marked");
        const ids = $chosen.map((_, item) => item.id).get().join(", ");
        $("#result").text(`${ids || "none"} | ${$chosen.length}`);
      }
      $("#rule").on("change", refresh);
      $("#prepend").on("click", function () {
        if (!$("#extra").length) {
          $("#group-a").prepend('<div id="extra">Notice</div>');
        }
        refresh();
      });
      $("#reset").on("click", function () {
        $("#extra").remove();
        $("#rule").val("2");
        refresh();
      });
      refresh();
    });
  </script>
</body>
</html>

初期の式は2です。両方の2番目の要素は最初のpなので、a1とb1が強調されます。

a1, b1 | 2
  1. 2n は a1, b1, b3 | 3 です。Aの4番目はspanなのでp条件から外れます。
  2. 2n+1 は a2, a3, b2 | 3 です。先頭のh2はpではありません。
  3. 3n+1 は b3 | 1 です。Aの1・4番目はh2とspanで、Bの4番目だけがpです。
  4. -n+3 は a1, a2, b1, b2 | 4 です。各親の最初の3要素にあるpを選びます。
  5. Reset の後 Prepend notice to A を押すと、Aの先頭にdivが入ります。式2ではAの2番目がh2になり、b1 | 1 だけになります。繰り返し押しても重複しません。
  6. Reset は追加divだけを除去し、式を2に戻して再検索します。元の内容と a1, b1 | 2 が戻ります。

:nth-of-type() と .eq() を比較する

Reset後、開発者ツールのConsoleで次の部分コードを実行します。:nth-of-type()は同じタグの兄弟だけを数え、.eq()は選択済み集合を0から数えます。

const ids = $set => $set.map((_, item) => item.id).get().join(", ");
console.log(ids($("#groups > .group > p:nth-child(2)")));
console.log(ids($("#groups > .group > p:nth-of-type(2)")));
console.log(ids($("#groups > .group > p").eq(1)));
a1, b1
a2, b2
a2

1行目は各親の2番目の要素でpでもあるa1・b1、2行目は各親の2番目のpであるa2・b2です。最後はp集合全体のインデックス1にあるa2だけです。.eq()の負数は集合末尾から数える指定で、-n+3とは異なります。

動的な文書と想定外の結果を調べる

選択結果は文書変更を自動反映しません。追加前のa1・b1を保存した集合は、追加後もその要素を保持します。現在の兄弟位置が必要ならrefreshのように再検索し、以前の強調も外します。

空の結果なら0や存在しない位置を指定していないか、その位置のタグがpか、間に見出し・案内・非表示要素がないかを確認します。表ではDOM(Document Object Model、文書オブジェクトモデル)のtbodyなど中間の親も確認します。反応しない場合は、まずNetworkのjQuery読み込みとConsoleのエラーを調べます。