🪜 sibling-index()で「JSなしスタガード」を自サイトに入れてみた — min()で頭打ちにするのがコツだった
CSS sibling-index()を使うと、リストの連番アニメーションがHTML変更ゼロ・JSゼロで書ける。実サイトの記事一覧に導入して分かった、遅延の頭打ち・@supports・reduced-motionの実装パターンを共有する。
リストの項目を上から順にふわっと出す、いわゆるスタガードアニメーション。これまでの選択肢は「HTMLに style="--i: 3" を1個ずつ振る」「JSで index * 100ms を流し込む」「nth-childで要素数ぶんCSSを書く」の三択で、どれを選んでも小さな敗北感があった。
sibling-index() はこれを終わらせる関数だ。要素が兄弟の何番目か(1始まり)を返すだけ。それだけなんだけど、animation-delay に掛け算するだけで連番遅延が完成する。このサイトのトップの記事一覧に実際に入れたので、その過程で決めたことを書いておく。
実装は10行で終わった
うちのトップページは .post-list の下に .post-card が並ぶだけの構造だ。そこにこう足した。
@keyframes card-in {
from { opacity: 0; translate: 0 12px; }
to { opacity: 1; translate: 0 0; }
}
.post-list > .post-card {
animation: card-in 0.45s cubic-bezier(0.2, 0.7, 0.3, 1) both;
animation-delay: min(calc((sibling-index() - 1) * 45ms), 500ms);
}
HTMLは1文字も触っていない。記事が増えても減っても、ブラウザが勝手に番号を振り直してくれる。ビルド時に連番を埋め込む必要すらない。
ポイントは min() で遅延を500msで頭打ちにしているところだ。最初は素直に sibling-index() * 45ms だけで書いたんですが、記事が30本あると最後のカードは1.3秒待ちになる。スクロールして初めて見える位置のカードが、見えた時点でまだ出現待ちをしているのは間抜けでしかない。ファーストビューの数枚だけ階段状に出て、それ以降はほぼ同時。これが体感として一番自然だった。
あと sibling-index() - 1 にしているのは、1始まりだから。そのまま使うと先頭カードにも45msの遅延が乗って、ページ全体がワンテンポ遅れて見える。地味だけど気づくまで数分悩んだ。
Firefoxがまだなので、@supportsで包む
対応状況は2026年7月時点でChrome 138以降・Edge・Safari 26.2。Firefoxが未対応で、Baselineには届いていない。なので全体をこう包んでいる。
@supports (animation-delay: calc(sibling-index() * 1ms)) {
@media (prefers-reduced-motion: no-preference) {
/* 上記のスタガード一式 */
}
}
非対応ブラウザではブロックごと無視されて、即時表示のまま。装飾アニメーションはこの「段階的強化」がいちばん気楽だ。Firefoxユーザーには申し訳ないが、出ないことに誰も気づかない類の演出なので、フォールバックを別途書くことはしなかった。prefers-reduced-motion を重ねているのは、動きを減らす設定の人に階段演出を見せる理由がないからです。
兄弟の「総数」も取れる
対になる sibling-count() を使うと兄弟要素の総数が取れるので、位置÷総数の比率が計算できる。放射状メニューの角度(rotate(calc(sibling-index() / sibling-count() * 360deg)))や、リストの先頭から末尾へ色相を回すグラデーションみたいな、これまでSassのループかJSでしか書けなかった系の表現がCSS単体に降りてくる。
なお、この関数自体の解説はICS MEDIAやQiitaに既に良い記事がある。この記事の持ち分は「実サイトに入れたら min() の頭打ちと -1 が必要だった」という運用の皺の部分だ。
インデックスを手で振る作業は、思えばずっと「HTMLとCSSの間の隙間を人間が埋める」仕事だった。その隙間がひとつ塞がった。あなたのプロジェクトの style="--i:"、もう消せるかもしれませんよ。