GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

CSS アニメーション 順番に表示 9選 — コピペで動く

css アニメーション 順番に表示は、リストやグリッドの項目を一斉にではなく並び順に沿ってわずかな遅延を足し、一つずつ現れさせたり消えさせたりする実装方法です。9個のうち1つ(08)は変数もループも使わず、CSSの関数一行だけで並び順を読み取って遅延を計算します。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並び順は演出の派手さではなく、遅延の求め方がどれだけ自動化されているかを基準に並べました。まずnth-childと数値の掛け算だけで軸を変える3つ(01〜03)、形や向きを作り込む3つ(04〜06)、そして遅延そのものの出し方を変える3つ(07〜09)という流れです。css アニメーション サンプル のコードは遅延を決める核心の行だけを抜き出しており、9つとも実際に動くファイル一式はこの記事の圧縮ファイルに入っています。 項目が止まらず流れ続けるべき場面では遅延ではなく無限ループが答えになりCSS 無限ループ マーキー9選の側になりますし、順番ではなく進み具合を見せるならプログレスバー css アニメーション 9選です。

01リストのカスケード

css アニメーション フェードイン の遅らせ方

リスト項目3つが下にわずかに沈んで縮んだ状態で待機し、nth-childごとに90ms遅れて元の大きさへ浮かび上がります。

どこで使うか

単純なフェードより奥行きが出るので、通知フィードや検索結果のように新着が積み上がる一覧に向いています。

リストのカスケード
新しいタブで開く

01 · リストのカスケード — 計測 累積面積 9.301% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scale+translateY90ms 간격ease-pop
.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を掛けた遅延を割り当てています。

どこで使うか

左上から右下へ対角線状に波打つ順で現れるので、ダッシュボードのカードやギャラリーのサムネイルに使いやすい並びです。

グリッドウェーブ
新しいタブで開く

02 · グリッドウェーブ — 計測 累積面積 11.562% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

행+열 인덱스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文字ずつ着地するので、ロゴのイントロやヒーローの見出しのように第一印象が勝負の場所に合います。

文字単位のスタガー
新しいタブで開く

03 · 文字単位のスタガー — 計測 累積面積 4.64% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

글자별 span30ms 간격translateY 바운스
.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に置かないと、ただの平らな回転になってしまいます。

カードの3D順次フリップ
新しいタブで開く

04 · カードの3D順次フリップ — 計測 累積面積 13.364% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotateY 90→0150ms 간격perspective
.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によって下から逆順に消えます。

どこで使うか

削除の確認リストや通知の片付けでは、後から来たものから先に消える方が自然です。

逆順で退場
新しいタブで開く

05 · 逆順で退場 — 計測 累積面積 6.794% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

역순 delay50ms 간격enter/exit 분리
@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で真っ先に膨らみ始めるので、波紋が中心から外へ広がって見えます。

円形の波紋
新しいタブで開く

06 · 円形の波紋 — 計測 累積面積 9.975% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

중심 거리 delay동심원 확산scale+opacity
.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も要らないので、項目数が増減してもスタイルを再コンパイルする必要がありません。

--i変数とcalc()一行
新しいタブで開く

07 · --i変数とcalc()一行 — 計測 累積面積 13.745% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

--i 변수calc() 한 줄인라인 style
// 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()対応表)。

sibling-index()で自動遅延
新しいタブで開く

08 · sibling-index()で自動遅延 — 計測 累積面積 5.736% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

sibling-index()반복문 없음Chrome/Edge 138+
.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回だけ決まって固定されます。

SCSS random()で疑似ランダム遅延
新しいタブで開く

09 · SCSS random()で疑似ランダム遅延 — 計測 累積面積 6.827% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SCSS random()비선형 지연컴파일타임 고정
@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構造を基準に計算されます)。

random() は一度きり

2つ目は09のrandom()で陥った勘違いです。この関数はsassがstyle.cssを作るときに一度だけ実行され、8dxrtqptが要る圧縮ファイルの中のファイルにも、その一度きりで決まった値がそのまま書き込まれています。だからページを開き直しても、バッジの並び順は毎回同じままです。

perspective は親に書く

3つ目は04のカードを組み立てていたときに気づいたことです。perspectiveを回転させたい.card自身に書いてしまうと奥行きがまったく付かず、rotateYの角度をどれだけ大きくしてもただの平らな板が回るだけでした。perspectiveを親の.cardsへ移し、.card側はtransform-style: preserve-3dとrotateYだけを持つ形に分けてから、カードらしく立体的にめくれるようになりました。 順に灯っていくこのリズムに光まで重ねたい場合は、CSS ネオン・グロー効果9選のネオンが同じ遅延の値とよく噛み合います。

アクセシビリティ

動きを減らす設定では

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

スタガーの css アニメーション 作り方 にJavaScriptは必須ですか?

いいえ、必須ではありません。9つとも@keyframesとanimation-delayだけで自動的に繰り返されます。07の--iはマークアップに埋め込んだインラインstyleの値で、08のsibling-index()もCSSの関数なので、どちらもJavaScriptの行は0行です。 同じ遅延の計算を縦に長く連ねて段階を見せる形は、タイムライン デザイン css 9選にまとめてあります。

--i変数とsibling-index()はどちらを選べばいいですか?

今すぐすべてのブラウザで安定させたいなら、07の--iとcalc()の組み合わせが無難です。Chrome/Edge 138以降だけで動けば十分な社内ツールや管理画面なら、08のsibling-index()のほうがマークアップを軽くできます(caniuseの対応表で確認してください)。

09のランダム遅延はページを開き直すたびに変わりますか?

変わりません。SCSSのrandom()はsassがstyle.cssを作る一度きりのタイミングでしか実行されず、決まった値がファイルに固定されます。開くたびに本当にランダムにしたい場合はCSSだけでは足りず、読み込み時にJavaScriptで--delayのようなカスタムプロパティを計算し直す必要があります。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。