はじめに (対象読者・この記事でわかること)

本記事は、DOM操作の基礎は把握しているが、特定のHTML要素が「親要素から見て何番目なのか」を正確に得たいフロントエンドエンジニア・Web制作者を対象とします。JavaScript(純JS)での実装を中心に、CSSの擬似クラスとの違い、テキストノード混入時の注意点、動的更新に強い書き方などを解説します。読み終えると、要素のインデックスを取得する複数のアプローチと、その選び方・性能・罠を理解し、堅牢な実装ができるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - 基本的なHTML/DOM構造の理解 - JavaScriptの配列メソッド(forEach, Array.from など)とNode/Elementの違い

背景と要点整理:DOMにおける「何番目か」をどう定義するか

「何番目か」の定義は文脈で変わります。主な観点は以下です。 - 子ノード全体の順番: childNodes は要素ノード以外(テキストノード、コメント)も含む - 子要素のみの順番: children または element.previousElementSibling 連鎖で要素ノードのみを数える - CSSの nth-child と nth-of-type の違い: - nth-child は「子ノードのうち要素ノードのみを対象、型は問わない」。テキストノードは対象外だが、兄弟の要素順はそのまま - nth-of-type は「同じタグ名の要素の中での順番」 - ランタイムの考慮: - ライブ vs 静的コレクション: NodeList/HTMLCollection の更新反映の有無 - パフォーマンス: siblings を遡る O(k) か、配列化して indexOf で O(n) か - セマンティクス: - 0始まり(プログラミング的)か、1始まり(人に見せる、CSS的)か

この記事では最も実用的な「子要素における1始まりのインデックス」を軸に、実装方法を比較します。合わせて「同型要素だけでのインデックス(nth-of-type相当)」や、アクセシビリティ・差分更新に強い書き方も紹介します。

実装手順とコード例:用途別ベストプラクティス集

ここが記事のメインパートです。ユースケース別に最適なコードを提示し、選定理由や落とし穴を解説します。

ステップ1: 子要素としての「何番目」(nth-child相当・1始まり)

目的: 親要素の直下にある「要素ノード」の並びで、対象要素が1,2,3...の何番目かを知る。

最速・低オーバーヘッド(兄弟を遡る):

Javascript
function elementIndex1Based(el) { let i = 1; while ((el = el.previousElementSibling) !== null) i++; return i; } // 使用例 const item = document.querySelector('#target'); console.log(elementIndex1Based(item)); // 1,2,3,...

特徴: - 要素ノードのみカウント(テキストノードの影響なし) - 追加の配列化が不要で、兄弟数が少なければ高速 - 動的更新に強く、常に最新のDOM状態を反映

読みやすさ重視(childrenとindexOf):

Javascript
function elementIndex1BasedReadable(el) { const parent = el.parentElement; if (!parent) return -1; return Array.prototype.indexOf.call(parent.children, el) + 1; }

特徴: - 可読性が高い - 兄弟要素が多いと indexOf の線形探索でコスト増

テキストノードを含めた「ノード順」(特殊ケース):

Javascript
function nodeIndex1BasedIncludingText(node) { let i = 1; while ((node = node.previousSibling) !== null) i++; return i; }

注意: - 一般に見た目の並び理解には不要。specialな解析やAST的用途向け

ステップ2: 同じタグ名の中での「何番目」(nth-of-type相当)

目的: 同じタグ名(例: li の中での3番目)での位置が必要な場合。

高速・簡潔(遡り版):

Javascript
function elementIndexOfType1Based(el) { const tag = el.tagName; let i = 1, p = el.previousElementSibling; while (p) { if (p.tagName === tag) i++; p = p.previousElementSibling; } return i; }

読みやすい配列化:

Javascript
function elementIndexOfType1BasedReadable(el) { const parent = el.parentElement; if (!parent) return -1; const same = parent.querySelectorAll(`:scope > ${el.tagName.toLowerCase()}`); // NodeListは静的。都度取得で最新状態 return Array.prototype.indexOf.call(same, el) + 1; }

ポイント: - :scope は親直下限定セレクタで誤爆を防ぐ - tagName は大文字、セレクタは小文字化が無難

ステップ3: 実運用での落とし穴と防御策

1) テキストノードの罠 - innerHTML で改行や空白を入れても previousElementSibling は影響を受けない - ただし previousSibling を使うと数えられてしまうので、要素限定なら previousElementSibling を必ず使う

2) 動的DOM更新とコレクションの性質 - parent.children はライブ(変更が即反映) - querySelectorAll は静的(取得時点のスナップショット) - 毎回最新値が必要なら取得タイミングを工夫。キャッシュするとズレる可能性

3) Shadow DOM/スロット - スロット配下や shadowRoot 内では、親子関係・セレクタの意味が変わる - シャドウ境界を跨いだ計算は基本できない。shadowRoot.host 単位で評価する

4) display: none と順序 - 見た目で非表示でも DOM の順序は変わらない - フィルタリングして「見える要素だけでの順位」を出したい場合は、computedStyle で display/visibility を考慮して独自に数える

Javascript
function visibleIndex1Based(el) { const parent = el.parentElement; if (!parent) return -1; let i = 0; for (const c of parent.children) { const cs = getComputedStyle(c); if (cs.display !== 'none' && cs.visibility !== 'hidden') { i++; if (c === el) return i; } } return -1; }

5) アクセシビリティ(A11y)との両立 - スクリーンリーダー向けに aria-posinset, aria-setsize を付与する場合は、「ユーザーが知覚する集合」での順番が望ましい - 「見えている要素」や「同タイプのみ」を集計して反映

Javascript
function applyAriaListPosition(container, itemSelector) { const items = Array.from(container.querySelectorAll(`:scope > ${itemSelector}`)) .filter(el => getComputedStyle(el).display !== 'none'); items.forEach((el, idx) => { el.setAttribute('aria-posinset', String(idx + 1)); el.setAttribute('aria-setsize', String(items.length)); }); }

6) パフォーマンス指針 - 単発計算: previousElementSibling 遡りが安定して速い - 順番を何度も参照: 一度配列化し、Map で逆引きすると多回呼びに強い

Javascript
function indexMap1Based(parent) { const arr = Array.from(parent.children); const map = new Map(arr.map((el, i) => [el, i + 1])); return el => map.get(el) ?? -1; }

7) イベント委譲との合わせ技 - クリック対象の「何番目」取得:

Javascript
const list = document.querySelector('#list'); list.addEventListener('click', e => { const item = e.target.closest('li'); if (!item || item.parentElement !== list) return; const idx = elementIndex1Based(item); console.log('clicked item index:', idx); });

ハマった点やエラー解決

問題1: indexOf が常に -1 を返す

const idx = Array.from(parent.children).indexOf(el); // -1 になる

原因: - el が parent の直下子でない - 取得時の parent が異なる(cloneやshadowRootなど) - el がまだ DOM に挿入されていない

解決策: - el.parentElement === parent をチェック - 直下判定には :scope > を使う - 挿入前なら、insert 後に計算する

問題2: nth-child の見た目とズレる

li:nth-child(3) とJS計算が一致しないように見える

原因: - nth-child は「要素ノード単位」。JS側で previousSibling を使っていた - 非表示要素をCSSで除外しているつもりで、DOM順序はそのまま

解決策: - previousElementSibling を使う - 「表示上の順位」を計算したいなら visibleIndex1Based を使う

問題3: ライブ/静的の混在で順序がブレる

const list = parent.children; // ライブ
const idx = Array.prototype.indexOf.call(list, el); // DOM変化中に不安定

解決策: - 安定さ重視なら Array.from(parent.children) でスナップショット化 - 順序決定の瞬間にのみ評価

まとめ

本記事では、DOMにおける「親から見た要素の順番」を、用途別に安全かつ効率よく求める方法を解説しました。 - 要素ベースの順位は previousElementSibling 遡りが軽量で堅牢 - nth-of-type 相当はタグ名でのフィルタを併用 - 見た目やA11y要件には「表示要素のみ」や aria-posinset などの配慮が必要

これらを使い分けることで、UIイベント処理、アクセシビリティ対応、差分更新など様々な局面で安定した順位判定が行えます。次回は、仮想リストやIntersectionObserverと組み合わせた大規模DOMでの最適化手法も扱う予定です。

参考資料