CSS アニメーション 順番に表示 9選 — コピペで動く
css アニメーション 順番に表示は、リストやグリッドの項目を一斉にではなく並び順に沿ってわずかな遅延を足し、一つずつ現れさせたり消えさせたりする実装方法です。9個のうち1つ(08)は変数もループも使わず、CSSの関数一行だけで並び順を読み取って遅延を計算します。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 リストのカスケード
- 02 グリッドウェーブ
- 03 文字単位のスタガー
- 04 カードの3D順次フリップ
- 05 逆順で退場
- 06 円形の波紋
- 07 --i変数とcalc()一行
- 08 sibling-index()で自動遅延
- 09 SCSS random()で疑似ランダム遅延
並び順は演出の派手さではなく、遅延の求め方がどれだけ自動化されているかを基準に並べました。まずnth-childと数値の掛け算だけで軸を変える3つ(01〜03)、形や向きを作り込む3つ(04〜06)、そして遅延そのものの出し方を変える3つ(07〜09)という流れです。コードは遅延を決める核心の行だけを抜き出しており、9つとも実際に動くファイル一式はこの記事の圧縮ファイルに入っています。
01リストのカスケード
リスト項目3つが下にわずかに沈んで縮んだ状態で待機し、nth-childごとに90ms遅れて元の大きさへ浮かび上がります。単純なフェードより奥行きが出るので、通知フィードや検索結果のように新着が積み上がる一覧に向いています。
.cascade li {
animation: cascade-loop $dur-loop $ease-pop infinite;
}
@for $i from 1 through 3 {
li:nth-child(#{$i}) {
animation-delay: ($i - 1) * 90ms;
}
}
@keyframes cascade-loop {
0%, 8% { opacity: 0; transform: translateY(14px) scale(.9); }
26%, 66% { opacity: 1; transform: translateY(0) scale(1); }
}
02グリッドウェーブ
3×3のグリッドカード9枚に、自分の行番号と列番号を足した値へ70msを掛けた遅延を割り当てています。左上から右下へ対角線状に波打つ順で現れるので、ダッシュボードのカードやギャラリーのサムネイルに使いやすい並びです。
$cols: 3;
@for $i from 1 through 9 {
$row: math.floor(math.div($i - 1, $cols));
$col: ($i - 1) % $cols;
.cell:nth-child(#{$i}) {
animation-delay: ($row + $col) * 70ms;
}
}
@keyframes wave-loop {
0%, 6% { opacity: 0; transform: scale(.5); }
22%, 62% { opacity: 1; transform: scale(1); }
}
03文字単位のスタガー
見出しの文字を1つずつspanで包み、30msずつ遅らせて下から跳ね上がらせています。文章がまとめて出るのではなく1文字ずつ着地するので、ロゴのイントロやヒーローの見出しのように第一印象が勝負の場所に合います。
.letters span {
animation: letter-loop $dur-loop $ease-pop infinite;
}
@for $i from 1 through 5 {
span:nth-child(#{$i}) {
animation-delay: ($i - 1) * 30ms;
}
}
@keyframes letter-loop {
0%, 4% { opacity: 0; transform: translateY(16px); }
20%, 66% { opacity: 1; transform: translateY(0); }
}
04カードの3D順次フリップ
カード3枚がY軸に90度倒れた状態で待ち、150msずつ遅れて1枚ずつrotateYで開いて正面を見せます。奥行きを作るperspectiveは回転する.card自身ではなく親の.cardsに置かないと、ただの平らな回転になってしまいます。
.cards {
perspective: 600px;
}
.card {
transform-style: preserve-3d;
animation: flip-loop $dur-loop $ease-out infinite;
}
@for $i from 1 through 3 {
.card:nth-child(#{$i}) {
animation-delay: ($i - 1) * 150ms;
}
}
@keyframes flip-loop {
0%, 6% { opacity: 0; transform: rotateY(90deg); }
26%, 66% { opacity: 1; transform: rotateY(0deg); }
}
05逆順で退場
トースト3つは登場するときは上から順に並びますが、消えるときはexit-1・exit-2・exit-3という個別のkeyframesによって下から逆順に消えます。削除の確認リストや通知の片付けでは、後から来たものから先に消える方が自然です。
@keyframes exit-1 {
0%, 5% { opacity: 0; transform: translateY(-8px); }
15%, 78% { opacity: 1; transform: translateY(0); }
90%, 100% { opacity: 0; transform: translateY(-8px); }
}
@keyframes exit-3 {
0%, 25% { opacity: 0; transform: translateY(-8px); }
35%, 53% { opacity: 1; transform: translateY(0); }
65%, 100% { opacity: 0; transform: translateY(-8px); }
}
06円形の波紋
3×3に並んだ点9個の遅延を、中央のマスまでの距離に90msを掛けて決めています。角のマスは対角距離(√2倍)ぶん最も遅く、中央は遅延0で真っ先に膨らみ始めるので、波紋が中心から外へ広がって見えます。
.ripple span {
animation: ripple-loop $dur-loop $ease-out infinite;
}
// 3×3、中央(5番目)までの距離 × $step — 角は√2、辺は1、中央は0
span:nth-child(1) { animation-delay: 90ms * 1.41; }
span:nth-child(5) { animation-delay: 0ms; }
span:nth-child(9) { animation-delay: 90ms * 1.41; }
@keyframes ripple-loop {
0%, 4% { opacity: 0; transform: scale(.3); }
24%, 58% { opacity: 1; transform: scale(1); }
}
07--i変数とcalc()一行
各要素のインラインstyleに--iの値だけを0から順に振っておけば、CSS側はループを1行も書かずにcalc(var(--i) * 80ms)だけで遅延を出せます。@forもnth-childも要らないので、項目数が増減してもスタイルを再コンパイルする必要がありません。
// html: <li style="--i:0">, <li style="--i:1">, ...
.i-list li {
animation: i-loop $dur-loop $ease-out infinite;
animation-delay: calc(var(--i) * 80ms);
}
@keyframes i-loop {
0%, 6% { opacity: 0; transform: translateY(10px); }
22%, 68% { opacity: 1; transform: translateY(0); }
}
08sibling-index()で自動遅延
タイル6枚には--iのようなインライン変数を1つも付けず、animation-delay: calc(sibling-index() * 100ms)という1行だけで兄弟内の順序を直接読み取らせています。マークアップは07よりさらに軽くなりますが、対応は今のところChrome/Edge 138以降に限られます(caniuseのsibling-index()対応表)。
.tiles span {
animation: tile-loop $dur-loop $ease-out infinite;
animation-delay: calc(sibling-index() * 100ms);
}
@keyframes tile-loop {
0%, 6% { opacity: 0; transform: scale(.5); }
22%, 68% { opacity: 1; transform: scale(1); }
}
09SCSS random()で疑似ランダム遅延
バッジ5個には、SCSSのrandom(420)関数でコンパイル時に1〜420msの遅延を項目ごとに割り振っています。均等な間隔ではなく散らばった登場になりますが、この値はブラウザを開くたびに変わるのではなく、sassがビルドした瞬間に1回だけ決まって固定されます。
@for $i from 1 through 5 {
.badge:nth-child(#{$i}) {
animation-delay: random(420) * 1ms;
}
}
@keyframes badge-loop {
0%, 4% { opacity: 0; transform: translateY(10px) scale(.9); }
16%, 60% { opacity: 1; transform: translateY(0) scale(1); }
}
どこで崩れるか — 落とし穴
9つとも遅延の算出方法は違っても、崩れ方には共通点がありました。1つ目は、01〜06と09のようにnth-childや距離を数値で決め打ちする方法です。これは項目数がぴったり固定されているときしか噛み合いません。リストに項目が動的に増減する場面では、07の--iインライン値か08のsibling-index()のようにDOM側の並びを直接読む方法を選ばないと、スタイルを再コンパイルするかマークアップを手で調整する羽目になります(MDNの:nth-child()の説明にあるとおり、この疑似クラスはコンパイル時ではなく実際のDOM構造を基準に計算されます)。
2つ目は09のrandom()で陥った勘違いです。この関数はsassがstyle.cssを作るときに一度だけ実行され、8dxrtqptが要る圧縮ファイルの中のファイルにも、その一度きりで決まった値がそのまま書き込まれています。だからページを開き直しても、バッジの並び順は毎回同じままです。
3つ目は04のカードを組み立てていたときに気づいたことです。perspectiveを回転させたい.card自身に書いてしまうと奥行きがまったく付かず、rotateYの角度をどれだけ大きくしてもただの平らな板が回るだけでした。perspectiveを親の.cardsへ移し、.card側はtransform-style: preserve-3dとrotateYだけを持つ形に分けてから、カードらしく立体的にめくれるようになりました。
アクセシビリティ
9つともprefers-reduced-motion: reduceではanimation: noneに置き換わり、opacity: 1とtransform: noneで最終的に見える状態のまま止まります。アニメーションが苦手な人には少し前から止まっていたように見えるだけで、リストや文字、カードの中身そのものは常にそこにあります。08のsibling-index()は未対応ブラウザだと関数自体が無効になるためanimation-delayは既定の0秒に戻りますが、tile-loop自体は動き続けるので、遅延だけが消えて6枚が同時に現れる形へ自然に崩れます。
@media (prefers-reduced-motion: reduce) {
li, span, .card, .badge {
animation: none !important;
opacity: 1;
transform: none;
}
}
順番に表示する以外のCSSアニメーションはCSSアニメーションのカテゴリーに、このブログの取り組みは紹介ページにまとめています。
FAQ
スタガーアニメーションを動かすのにJavaScriptは必須ですか?
いいえ、必須ではありません。9つとも@keyframesとanimation-delayだけで自動的に繰り返されます。07の--iはマークアップに埋め込んだインラインstyleの値で、08のsibling-index()もCSSの関数なので、どちらもJavaScriptの行は0行です。
--i変数とsibling-index()はどちらを選べばいいですか?
今すぐすべてのブラウザで安定させたいなら、07の--iとcalc()の組み合わせが無難です。Chrome/Edge 138以降だけで動けば十分な社内ツールや管理画面なら、08のsibling-index()のほうがマークアップを軽くできます(caniuseの対応表で確認してください)。
09のランダム遅延はページを開き直すたびに変わりますか?
変わりません。SCSSのrandom()はsassがstyle.cssを作る一度きりのタイミングでしか実行されず、決まった値がファイルに固定されます。開くたびに本当にランダムにしたい場合はCSSだけでは足りず、読み込み時にJavaScriptで--delayのようなカスタムプロパティを計算し直す必要があります。