はじめに: なぜ今 sibling-index() なのか
フロントエンド開発者であれば、一度は経験したことがあるはずです。カードグリッドに順次フェードインアニメーションを適用したい。単純な要件なのに、実装するたびに「もっと良い方法があるはずだ」と感じていたあの感覚です。
これまでの選択肢は限られていました。
- Sass ループで10個、20個の
:nth-child()ルールを生成する。リストが50に増えれば、CSS ファイルサイズも比例して増大します。 - JavaScript で DOM を走査し、
style="--index: 3"のようなインラインスタイルを注入する。動作はしますが、半年後にリファクタリングする際、「この CSS はどこから注入されているのか?」と原因特定に時間を浪費することになります。
どちらのアプローチも、根本的に同じ問題を抱えていました。ブラウザはすでに DOM ツリーを知っています。 どの要素が何番目の子要素か、親が全部でいくつの子要素を持っているか、すべて把握している。それなのに、CSS がその情報を活用する方法がなかったのです。
今、状況は変わりました。CSS Values and Modules Level 5 仕様(セクション9)に含まれる sibling-index() と sibling-count() により、たった一行の CSS でこの問題が解決します。
li {
animation-delay: calc(sibling-index() * 100ms);
}
このコード一行で、5個のアイテムでも5,000個のアイテムでも問題なく動作します。イベントリスナー、MutationObserver、再レンダリングは一切不要です。
本記事では、この2つの関数の正確な動作から、実務で即活用できるパターン、そして必ず知っておくべき注意点までを包括的に解説します。

基本概念: sibling-index() と sibling-count()
両関数とも引数を取らず、それぞれ以下の値を返します。
sibling-index(): 親の子要素の中で、現在の要素が何番目かを1から始まる整数で返します。テキストノード、コメント、空白は無視され、要素ノードのみがカウントされます。sibling-count(): 親が持つ子要素の総数を返します。JavaScript のelement.parentElement.children.lengthと正確に同じ値ですが、CSS スタイルシート内で直接使用できます。
両関数は <integer> 値として評価されるため、calc()、min()、max()、round()、mod()、さらには三角関数の sin()、cos() と自由に組み合わせることが可能です。
counter()との違い:counter()は文字列を返し、contentプロパティ内でしか使用できません。一方sibling-index()は整数なので、すべての計算プロパティで使用できます。
:nth-child()との違い::nth-child()はセレクタです。要素を選択するものであり、値を生成するものではありません。calc(:nth-child() * 10px)のような構文は無効です。sibling-index()は宣言部の中で計算に使用する数値を提供します。
実践パターン5選
1. 逆方向スタッガー (Reverse Stagger)
最後のアイテムを最初にアニメーションさせたい場合、sibling-count() から sibling-index() を引くだけです。
.card {
animation: fade-in 0.4s ease both;
animation-delay: calc((sibling-count() - sibling-index()) * 80ms);
}
最後のカードは (N - N) * 80ms = 0ms で即座に実行され、最初のカードは (N - 1) * 80ms で最も遅く実行されます。ページロード時の間延びした待ち時間がなくなり、即座にアニメーションが開始されます。
2. 自動均等幅 (Automatic Equal Widths)
子要素の数を数えてパーセンテージ値をハードコーディングする必要はもうありません。
.tab {
width: calc(100% / sibling-count());
}
タブが5つなら20%、6つなら16.66%、2つなら50%が自動計算されます。メディアクエリ、ResizeObserver、JavaScript は一切不要です。
注意: アイテムが多すぎるとタブ幅が狭くなりすぎる場合があります。そのような場合は Flexbox の
flex-wrapを検討してください。
3. 色相分布 (Hue Distribution)
カラーホイールに沿って均等に色を分配します。
.swatch {
background-color: hsl(
calc((360deg / sibling-count()) * sibling-index()) 70% 50%
);
}
アイテムが3つなら120度間隔、12個なら30度間隔で色が配置されます。DOM の構成に応じてパレットが自動調整されるため、JavaScript のカラーライブラリを使用する必要はありません。
4. 円形メニュー (Circular Menus)
CSS ネイティブの sin() と cos() 関数を sibling-index() と組み合わせることで、円形レイアウト全体をピュア CSS で実装できます。
.radial-item {
--angle: calc((360deg / sibling-count()) * sibling-index());
--radius: 120px;
position: absolute;
left: calc(50% + var(--radius) * cos(var(--angle)));
top: calc(50% + var(--radius) * sin(var(--angle)));
transform: rotate(calc(var(--angle) * -1));
}
アイテムが6つなら六角形、8つなら八角形になります。アイテムの追加・削除に応じてレイアウトが自動再計算されます。JavaScript で座標を計算する必要はまったくありません。
5. Z-Index スタッキング (Card Fan)
カードを重ねるエフェクトを一行で実現します。
.card {
z-index: calc(sibling-count() - sibling-index());
}
最初のカードが最前面に、最後のカードが 0 になります。順序を逆にしたい場合は sibling-index() のみを単独で使用します。

注意点と落とし穴 (Gotchas)
これらの関数は強力ですが、いくつか必ず理解しておくべき制約があります。
1. Shadow DOM スコーピング
sibling-index() と sibling-count() は DOM ツリー を基準に動作し、フラット化されたビジュアルツリー を基準にはしません。この違いは Web Components で決定的な影響を及ぼします。
- カスタム要素の Shadow DOM 内部で
sibling-index()を使用すると、常に Shadow DOM 内部の子だけがカウントされます。Light DOM からプロジェクションされたコンテンツはカウントに含まれません。 - Light DOM のスタイルシートが
::part()を介してコンポーネント内部にアクセスしsibling-index()を使用しようとすると、ブラウザは0を返します。これは意図的なセキュリティ対策であり、外部 CSS がサードパーティコンポーネントの内部構造を調査することを防ぎます。
2. display: none はカウントされる
これが最も混乱しやすいポイントです。display: none はレイアウトツリーから消え、画面に表示されず、スクリーンリーダーも読み上げませんが、DOM には依然として存在します。
sibling-index() は DOM ツリーを読み取るため、display: none の要素もカウントに含まれます。
<ul>
<li>Apple</li>
<li style="display: none;">Banana</li> <!-- 非表示だが DOM には存在 -->
<li>Cherry</li>
</ul>
この場合、Cherry の sibling-index() は 2 ではなく 3 になります。検索フィルターのように display: none で非マッチ項目を隠す場合、スタッガーアニメーションや円形レイアウトに 隙間 (Gap) が生じます。
解決策: 連続的なカウンティングが必要な場合(円形メニュー、比例幅など)は、
display: noneではなく実際に DOM からノードを削除するか、JavaScript でインデックスを管理するフォールバックを使用してください。
3. カスタムプロパティは即時評価される
親要素に --idx: sibling-index(); と宣言すると、--idx は親自身の兄弟インデックスで即座に評価され固定されます。子要素がこの値を継承すると、すべての子が同じ数値を持つことになり、意図した通りに動作しません。
/* 誤った例 */
.parent {
--idx: sibling-index(); /* 親のインデックスで固定される */
}
/* 正しい例: 関数を使用する要素に直接適用 */
.child {
--idx: sibling-index();
animation-delay: calc(var(--idx) * 100ms);
}
4. パフォーマンス: 大規模 DOM におけるコスト
DOM を変更(子の追加・削除・順序変更)すると、影響を受ける兄弟要素のスタイル再計算がトリガーされます。ブラウザはこの処理をレイアウトとペイントの前のカスケード段階で行うため、JavaScript でインラインスタイルを書き込む方式よりは高速です。
ただし、10,000個の子を持つコンテナの先頭に要素を挿入すると、エンジンは後続の10,000個すべての要素の兄弟インデックスを再計算する必要があります。通常のナビゲーション、カードグリッド、タブバーでは体感できませんが、リアルタイム株価ティッカーや無限スクロールフィードのように数千のノードが絶えず変化する環境では、JavaScript でインデックスを管理する方が依然として優れている場合があります。
ブラウザサポートとフォールバック戦略
- Chrome/Edge 138: 2025年6月から安定版でサポート
- Safari 26.2: サポート
- Firefox: 安定版には未搭載ですが、Mozilla の仕様ポジションは肯定的で、Bugzilla イシュー #1953973 で実装作業が進行中です。
Chrome と Safari のシェアを合わせると、グローバルトラフィックの約75〜80%をカバーします。ただし、Firefox ユーザーのためのフォールバックは必須です。
/* すべてのブラウザで動作するベーススタイル */
.item {
width: 25%;
animation-delay: 0ms;
}
/* サポートするブラウザでのみプログレッシブエンハンスメント */
@supports (z-index: sibling-index()) {
.item {
width: calc(100% / sibling-count());
animation-delay: calc(sibling-index() * 80ms);
}
}
参考資料: 本記事の元となった記事は Smashing Magazine の原文 でご確認いただけます。
次のステップとしての学習方向
- CSS
@propertyとinherits:sibling-index()をカスタムプロパティに代入する際の動作をより深く理解するには、CSS Houdini の@propertyルールを学習してみてください。 - CSS 三角関数 (
sin(),cos()): 円形レイアウト以外にも、様々な幾何学的パターンに応用できます。 aria-posinsetとaria-setsize: 視覚的な順序とアクセシビリティツリーの順序が異なる場合、ARIA 属性をどのように同期させるか検討してみてください。

まとめ: いよいよ CSS が DOM を読む時代へ
<div> を10個並べて、それぞれに :nth-child() ルールを10個書いていた時代は終わりました。sibling-index() と sibling-count() は、ブラウザがすでに知っている情報を CSS が活用できるようにする、長く待ち望まれた機能です。
これらの関数は、特別なライブラリやフレームワークを必要とせずに、レスポンシブレイアウト、動的アニメーション、自動色相分布などをピュア CSS で実現することを可能にします。
ただし、Shadow DOM でのスコーピング、display: none のカウンティング、大規模 DOM でのパフォーマンスコストなど、いくつかの注意点を必ず理解しておく必要があります。@supports を活用したプログレッシブエンハンスメント戦略で、Firefox ユーザーまでカバーすることを忘れないでください。
今すぐすべてのプロジェクトで使えるわけではありませんが、Chrome と Safari でサポートが始まり、Firefox も実装中です。2025年後半から本格的に実務導入の準備を始めても良いタイミングと言えるでしょう。