CSSで、要素を順番にふわっと表示する方法
ページを開いたときに、カードやリストの項目を上から1つずつ、透明な状態から見せていくには、同じアニメーションを全部の要素に付け、animation-delay(始まるまでの待ち時間)だけを要素ごとに少しずつずらします。JavaScript は要りません。待っている間に要素が見えてしまわないよう、animation に both を付けるのが要点です。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、Web Animations API でアニメーションを止めて、時刻を変えては各要素の opacity と translate の計算値を読みました。動きを減らす設定は、Playwright の emulateMedia() で再現しています。Firefox と Safari、スマートフォンの実機は試していません。スクロールして画面に入ったときに出す方法は、別の記事「CSSで、スクロールに合わせて要素をふわっと表示する方法」で扱っています。
待ち時間を 0.15 秒ずつずらす
まず、透明で少し下にずれた状態から、元の位置に戻るまでの動きを @keyframes で書きます。
@keyframes fade-in {
from {
opacity: 0;
translate: 0 16px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.stagger > * {
animation: fade-in 0.6s ease-out both;
animation-delay: calc(var(--i, 0) * 0.15s);
}
<ul class="stagger">
<li style="--i: 0">カード 1</li>
<li style="--i: 1">カード 2</li>
<li style="--i: 2">カード 3</li>
<li style="--i: 3">カード 4</li>
<li style="--i: 4">カード 5</li>
</ul>
各項目には、上から何番目かを --i というカスタムプロパティで持たせます。calc(var(--i, 0) * 0.15s) で、待ち時間が 0 秒、0.15 秒、0.3 秒…と増えていきます。animation-delay は、アニメーションが要素に付いてから実際に始まるまでの待ち時間を指定するプロパティです(MDN の animation-delay のページより)。
検証環境で計算値を読むと、5つの項目の animation-delay は 0s 0.15s 0.3s 0.45s 0.6s でした。動きの長さ 0.6 秒に最後の待ち時間 0.6 秒を足した 1.2 秒で、全部の項目の不透明度が 1 になりました。

この GIF では、ボタンを押すたびに、カード 1 から順に下から浮かび上がってきます。たとえばアニメーションを付けてから 0.45 秒の時点では、カード 1〜3 の不透明度は 0.91、0.68、0.38 で、カード 4 と 5 はまだ 0 のままです。
both を付けないと、待っている間は見えたまま
animation の最後の both は、animation-fill-mode の値です。これを書かないと、順番を待っている項目は最初から見えていて、自分の番が来た瞬間に消え、そこからふわっと出てくる、という動きになります。
/* うまくいかない書き方 */
.no-fill > * {
animation: fade-in 0.6s ease-out;
animation-delay: calc(var(--i, 0) * 0.15s);
}

待ち時間 0.3 秒のカード 3 で比べました。both を付けた場合は、0.3 秒までずっと不透明度 0 で、そこから上がり始めます。both がない場合は、0.3 秒まで不透明度 1 のまま見えていて、0.3 秒ちょうどで 0 に落ち、そこから上がり始めました。待っている間の translate も、both ありでは 0px 16px(下にずれた状態)、なしでは none でした。
MDN の animation-fill-mode のページによると、既定値の none では、アニメーションが動いていない間はキーフレームのスタイルが当たりません。backwards は、最初のキーフレームの値を待ち時間の間も当て続けます。both は、backwards と、終わった後に最後のキーフレームの値を保つ forwards の両方の働きをします。both にしておけば、to に元と違う値を書いたときも、終わった後はその値のまま止まります。
animation-delay は animation より後に書く
animation-delay を animation の一括指定より前に書くと、待ち時間が全部 0 秒になり、全部の項目が同時に出てきます。
/* うまくいかない書き方 */
.delay-first > * {
animation-delay: calc(var(--i, 0) * 0.15s);
animation: fade-in 0.6s ease-out both;
}
検証環境では、この書き方の animation-delay の計算値は 0s 0s 0s 0s 0s で、0.1 秒の時点の不透明度は5つとも 0.26 でした。MDN の animation のページによると、一括指定の animation に含めなかった値は初期値に戻ります。animation-delay の初期値は 0s なので、前に書いた待ち時間が消えてしまいます。
sibling-index() で、番号を HTML に書かずに済ませる
項目ごとに style="--i: 2" と書くのは手間で、項目を足したり並べ替えたりするたびに書き直しが要ります。sibling-index() を使うと、要素が兄弟の中で何番目かを CSS の中で数えられます。
.stagger > * {
animation: fade-in 0.6s ease-out both;
animation-delay: calc((sibling-index() - 1) * 0.15s);
}
MDN の sibling-index() のページによると、この関数は :nth-child() と同じく 1 から数えます。1つ目の項目の待ち時間を 0 秒にするため、1 を引いています。検証環境では、--i を使った書き方とまったく同じ待ち時間(0s 0.15s 0.3s 0.45s 0.6s)と不透明度の変化になりました。
ただし、MDN の互換性データ(@mdn/browser-compat-data 8.1.8)では、sibling-index() が使えるのは Chrome と Edge が 138 から、Firefox が 154 から、Safari が 26.2 からです。それより古いブラウザにも順番に出したいなら、--i の書き方を先に書き、sibling-index() の書き方を後に重ねます。
.stagger > * {
animation: fade-in 0.6s ease-out both;
animation-delay: calc(var(--i, 0) * 0.15s);
animation-delay: calc((sibling-index() - 1) * 0.15s);
}
MDN の CSS のエラー処理の解説によると、ブラウザは値を解釈できない宣言を無視します。sibling-index() を知らないブラウザでは2行目が捨てられ、1行目の --i の待ち時間が残ります。
対応していないブラウザは検証環境に用意していないので、2行目を存在しない関数(no-such-index())に変えて確かめました。CSS.supports() でこの値が false になることを確かめたうえで計算値を読むと、待ち時間は --i の書き方と同じ 0s 0.15s 0.3s 0.45s 0.6s でした。
項目が多いときは、待ち時間に上限を付ける
項目が 20 個あると、最後の項目は 0.15 秒 × 19 = 2.85 秒待ってから出てきます。下のほうの項目がなかなか表示されないのを避けるには、min() で、何番目かの数に上限を付けます。
.stagger > * {
animation: fade-in 0.6s ease-out both;
animation-delay: calc(min(sibling-index() - 1, 4) * 0.15s);
}
8つの項目で試すと、待ち時間は 0s 0.15s 0.3s 0.45s 0.6s 0.6s 0.6s 0.6s になりました。5つ目から後は同じ 0.6 秒後に一緒に出始め、1.2 秒で全部の不透明度が 1 になっています。
もう一度再生するには、クラスを付け直す
一度終わったアニメーションをもう一度動かすには、アニメーションを付けているクラスをいったん外して付け直します。ただし、外してすぐ付け直すだけでは動きません。
const list = document.querySelector('.stagger');
list.classList.remove('stagger');
void list.offsetWidth; // この1行がないと、もう一度は動かない
list.classList.add('stagger');
検証環境で、クラスを外してすぐ付け直したときは、動いているアニメーションが 0 個で、不透明度は 1 のままでした。間に offsetWidth を読む1行を入れると、5つのアニメーションが動き出し、不透明度はいったん全部 0 になってから、2 秒後には全部 1 に戻りました。上の GIF の「もう一度再生」ボタンも、この3行で動かしています。
動きを減らす設定の人には、最初から見せる
OS の設定で動きを減らしている人には、アニメーションを付けずに最初から見せます。prefers-reduced-motion は、動きを減らす設定が有効かどうかを調べるメディア特性です。
@media (prefers-reduced-motion: no-preference) {
.stagger > * {
animation: fade-in 0.6s ease-out both;
animation-delay: calc((sibling-index() - 1) * 0.15s);
}
}
動きを減らす設定を Playwright で再現したページでは、アニメーションの数が 0 で、5項目とも最初から不透明度 1 のまま表示されました。translate も none で、下にずれることもありません。設定がないときは、@media で囲む前と同じく 0.15 秒ずつずれて出てきました。
opacity: 0 は @keyframes の中だけに書き、項目そのもののスタイルには書かないでおきます。こうしておけば、アニメーションが付かないときは透明にならず、そのまま表示されます。





