はじめに:「なぜ今までできなかったんだ」という感覚
カードグリッドに1つずつフェードインするエフェクト——見た目は素晴らしいですが、実装するたびに「もっとスマートな方法があるはず」と感じていた方も多いのではないでしょうか。10個のアイテムに階段状の遅延アニメーション(staggered animation)を適用するには、従来は2つの選択肢しかありませんでした。
選択肢1: nth-child のハードコーディング
/* アイテムごとに1ルール。リストが増えないことを祈る */
li:nth-child(1) { --idx: 1; }
li:nth-child(2) { --idx: 2; }
li:nth-child(3) { --idx: 3; }
/* ... あと7つ ... */
li {
animation-delay: calc(var(--idx) * 100ms);
}
10個なら10行、50個なら? Sassループでビルド時に50ルールを生成するか、「10個でいいや」と妥協するしかありませんでした。
選択肢2: JavaScriptでインラインスタイル注入
items.forEach((el, i) => el.style.setProperty('--index', i));
正常に動作しますが、6ヶ月後に誰かがコンポーネントをリファクタリングする際、CSSがJavaScriptの変数に依存していることを忘れてしまい、静かに壊れます。
両方のアプローチに共通する根本的な問題:ブラウザはすでにDOMツリーを把握しているのに、CSSがその情報にアクセスできなかったため、迂回していたのです。
そして、その壁がついに取り払われました。
li {
animation-delay: calc(sibling-index() * 100ms);
}
たった1行。5個でも5000個でも動作します。イベントリスナーも、MutationObserverも、再レンダリングも不要です。

sibling-index() と sibling-count() 完全理解
基本概念
これらの関数はCSS Values and Units Module Level 5仕様(Section 9)の一部です。CSSWG issue #4559で長い議論を経て承認されました。
sibling-index(): 親の子要素における現在の要素の1ベースの位置を返します。最初の子は1、5番目は5。テキストノードやコメントは無視され、要素ノードのみがカウントされます。sibling-count(): 親が持つ子要素の総数を返します。JavaScriptのelement.parentElement.children.lengthと同等です。
両関数とも <number> 型として解決されるため、calc()、min()、max()、round()、sin()、cos() など、あらゆる計算コンテキストで即座に使用できます。
⚠️ 混同注意:
:nth-child()はセレクターです。要素を選択するだけで値を返しません。calc(:nth-child() * 10px)は無効なCSSです。一方、sibling-index()は値生成関数であり、宣言部の中で数値を提供します。
実践パターン6選
1. 階段アニメーション (Staggered Fade-in)
.card {
animation: fadeIn 0.4s ease both;
animation-delay: calc(sibling-index() * 80ms);
}
2. 逆方向階段アニメーション (Reverse Stagger) - 最後のアイテムから
.card {
animation: fadeIn 0.4s ease both;
animation-delay: calc((sibling-count() - sibling-index()) * 80ms);
}
最後のカードが最初に表示され、最初のカードが最も遅く表示されます。
3. 自動均等幅 (Equal Widths)
.tab {
width: calc(100% / sibling-count());
}
タブが5つなら20%、6つなら16.66%、4つなら25%。メディアクエリもJSも不要で自動調整されます。
4. 色相の均等分布 (Hue Distribution)
.swatch {
background-color: hsl(
calc((360deg / sibling-count()) * sibling-index()) 70% 50%
);
}
3アイテムなら120°間隔、12アイテムなら30°間隔。DOMに応じてパレットが自動適応します。
5. 円形メニュー (Circular Menu) - ピュア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アイテムなら八角形。追加・削除に応じてレイアウトが自動再計算されます。
6. Z-Index スタッキング (Card Fan)
.card {
z-index: calc(sibling-count() - sibling-index());
}
最初のカードが最上位、最後のカードが 0。順序を逆にしたい場合は sibling-index() のみを使用します。

注意点と落とし穴(これを知らないとデバッグ地獄)
Shadow DOM スコーピング
sibling-index() と sibling-count() はDOMツリーを基準に動作し、フラットなビジュアルツリーは参照しません。Web Components ではこの違いが決定的です。
<!-- カスタム要素のShadow DOM -->
<host-element>
#shadow-root
<style></style>
<div class="internal"></div>
.internal に sibling-index() を適用すると、常に 2 を返します。<style> と .internal の2つの子しかないからです。プロジェクトに300個の <project-item> がlight DOMに投影されていても、shadowツリー内では sibling-count() は 2 です。
また、::part() を通じて外部スタイルシートがコンポーネント内部を探索しようとすると、sibling-index() は 0 を返します。これは意図的なセキュリティ障壁です。
display: none もカウントされる
これが最も狡猾な落とし穴です。display: none はレイアウトツリーから消えますが、DOMツリーには残ります。sibling-index() はDOMツリーを読むため、非表示要素もインデックスに含まれます。
<ul>
<li>Apple</li>
<li style="display: none">Banana</li> <!-- 見えないがindexは2 -->
<li>Cherry</li> <!-- indexは3、2ではない -->
</ul>
検索フィルターで display: none を使用している場合、階段アニメーションや円形メニューに**隙間(gap)**が生じます。解決するには、フィルタリングされたノードを実際にDOMから削除するか、JavaScriptでインデックスを管理する必要があります。
カスタムプロパティの即時評価問題
/* 誤った例 */
.parent {
--idx: sibling-index(); /* .parent自身の兄弟インデックスで固定 */
}
.child {
animation-delay: calc(var(--idx) * 100ms); /* すべての子が同じ値を継承 */
}
解決策は簡単です。関数を適用対象の要素に直接記述してください。
.child {
--idx: sibling-index();
animation-delay: calc(var(--idx) * 100ms);
}
パフォーマンス:大規模DOMにおけるコスト
10,000子要素の先頭に要素を挿入すると、ブラウザは残り9,999要素の兄弟インデックスをすべて再計算する必要があります。通常のナビゲーション、カードグリッド、タブバーでは全く問題になりませんが、リアルタイム株価ティッカーや無限スクロールフィードのように数千ノードが絶えず変動する環境では、JavaScript管理のインデックスを維持する方が安全です。
ブラウザサポート状況
- Chrome/Edge 138(2025年6月):安定版でサポート
- Safari 26.2:サポート
- Firefox:未サポート(Bugzilla #1953973で実装進行中、ポジティブ)
Chrome + Safari の組み合わせでグローバルトラフィックの約75〜80%をカバーします。Firefoxユーザー向けのフォールバックは @supports で処理してください。
/* 全ブラウザで動作するベースライン */
.item {
width: 25%;
animation-delay: 0ms;
}
/* サポートするブラウザでのみ数学的レイアウトを有効化 */
@supports (z-index: sibling-index()) {
.item {
width: calc(100% / sibling-count());
animation-delay: calc(sibling-index() * 80ms);
}
}
アクセシビリティ注意事項
これらの関数は純粋に視覚的です。sibling-index() で order やグリッド配置を変更しても、スクリーンリーダーはDOMソース順を読み、キーボードタブ順もDOMに従います。視覚的レイアウトと意味的構造が矛盾すると、アクセシビリティ違反になります。インタラクティブコンポーネント(データグリッド、ラジアルメニューなど)では、引き続きJavaScriptで aria-posinset と aria-setsize を同期する必要があります。

日本の開発現場での適用コンテキスト
国内ではまだ sibling-index() と sibling-count() に関する情報は限られています。多くのCSS解説記事やブログでは、未だに :nth-child() セレクターとSassループを用いた階段アニメーションの手法が主流です。
実務適用のヒント:
- ダッシュボードUI: KPIカードグリッドへの階段状出現アニメーションに真価を発揮します。データ量が動的に変化してもCSSだけで対応可能です。
- ECサイトの商品一覧: フィルタリング結果がリアルタイムで変わる環境で、商品カードの幅を自動調整する用途に活用してください。
- レガシープロジェクトへの導入: 段階的に導入する場合は
@supportsフォールバックを必ず使用しましょう。まだ全ブラウザがサポートしているわけではないため、「壊れない」プログレッシブエンハンスメントが鍵です。
この技術の限界
display: noneフィルタリングとの相性が良くありません。検索・フィルター機能があるリストでは、JavaScriptで実際にDOMを削除する方式を検討する必要があります。- Shadow DOM内部では期待と異なる動作をする可能性があります。Web Componentsを使用中の場合は必ずテストしてください。
- 10,000要素以上の大規模動的リストではパフォーマンスコストが発生する可能性があります。
次のステップとしての学習方向
- CSS
@propertyの学習:カスタムプロパティの型と継承を制御する方法を習得すると、sibling-index()との組み合わせがさらに強力になります。 - CSS三角関数(
sin()、cos()) のマスター:円形レイアウトだけでなく、波形アニメーションや曲線パスなど、多彩な表現が可能になります。 - CSSコンテナークエリとの組み合わせ:
sibling-count()で取得した個数に応じてコンテナスタイルを動的に変更するパターンを研究してみてください。
合わせて読みたい記事
本記事は Smashing Magazineの原文 を基に、日本の開発者向けに再構成しました。