Stagger & Complex Sequences — Web Animation

Stagger & Complex Sequences Stagger animation adalah teknik menganimasi sekumpulan elemen dengan delay yang berurutan, menciptakan efek "domino" yang terasa…

Stagger berarti beberapa elemen menjalankan animasi yang sama, tapi mulainya bergiliran dengan jeda tetap. Sequence berarti animasi berikutnya baru mulai setelah yang sebelumnya selesai. Bayangin tari Saman: semua penari melakukan gerakan yang sama, tapi gelombangnya berjalan dari ujung barisan ke ujung lain, satu penari sedikit setelah yang lain (stagger). Setelah gelombangnya selesai, barulah syair berikutnya dimulai (sequence). Di halaman Bu Sri, tiga kartu kamar masuk bergiliran 60 ms, lalu toast muncul setelah kartu terakhir mendarat.

🧪 Tebakan cepat: Kamar Teras, Kamar Loteng, dan Kamar Kebun masuk masing-masing 300 ms, berselang 60 ms. Kapan Kamar Kebun mulai bergerak, dan kapan seluruh urutannya selesai? Jawabannya ada di bagian "Menghitung waktu stagger".

Menghitung waktu stagger

Delay kartu ke-i (dihitung dari 0) = delay awal + i × jeda. Waktu selesai = delay-nya + durasi:

Kartu i Mulai Selesai
Kamar Teras 0 0 ms 300 ms
Kamar Loteng 1 60 ms 360 ms
Kamar Kebun 2 120 ms 420 ms

Itu jawaban tebakannya: Kamar Kebun mulai di 120 ms, dan urutannya selesai di 420 ms = 2 × 60 + 300. Rumus umumnya: total = delay awal + (jumlah - 1) × jeda + durasi.

Urutan gak harus dari depan. Yang berubah cuma "jarak" tiap elemen dari titik mulainya:

from Jarak elemen ke-i 5 elemen, delay awal 200, jeda 50
"start" i 200, 250, 300, 350, 400
"end" (jumlah - 1) - i 400, 350, 300, 250, 200
"center" Math.abs(i - (jumlah - 1) / 2) 300, 250, 200, 250, 300
function createStagger(items, options) {
  const { delay, staggerAmount, from } = options;
  const last = items.length - 1;
  return items.map((item, i) => {
    let steps = i;                                        // "start": urutan biasa
    if (from === 'end') steps = last - i;                 // yang terakhir paling dulu
    if (from === 'center') steps = Math.abs(i - last / 2); // yang tengah paling dulu
    return { item, delay: delay + steps * staggerAmount };
  });
}

Motion menghitung dengan rumus yang sama: stagger(0.06, { from: 'center' }) = startDelay + 0.06 × |titikMulai - i| detik. GSAP juga: stagger: 0.15 berarti elemen ke-5 (indeks 4) mulai di 4 × 0,15 = 0,6 detik. Di Motion 12, staggerChildren di transition induk masih jalan tapi sudah ditandai deprecated; penggantinya delayChildren: stagger(0.06) (pelajaran Framer Motion Basics).

Stagger dengan CSS: --i dan calc()

CSS gak tahu urutan elemen, jadi kita titipkan lewat custom property:

<ul class="rooms">
  <li class="room" style="--i: 0">Kamar Teras</li>
  <li class="room" style="--i: 1">Kamar Loteng</li>
  <li class="room" style="--i: 2">Kamar Kebun</li>
</ul>
.room {
  animation: masuk 300ms ease-out both;        /* both: tersembunyi selama menunggu delay */
  animation-delay: calc(var(--i) * 60ms);      /* 0, 60, 120 ms */
}
@keyframes masuk { from { opacity: 0; translate: 0 16px; } }

sibling-index(): urutan tanpa --i

CSS Values 5 menambah sibling-index() (urutan elemen di antara saudaranya) dan sibling-count() (jumlah saudaranya). Hasilnya mulai dari 1, bukan 0:

.room { animation-delay: calc(var(--i, 0) * 60ms); }              /* cadangan */
.room { animation-delay: calc((sibling-index() - 1) * 60ms); }    /* 0, 60, 120 ms */

Dukungannya: Chrome 138 (24 Juni 2025), Safari 26.2 (12 Desember 2025), dan Firefox 154 (18 Agustus 2026). Kami cek dengan getAnimations(): Chromium 147 dan WebKit 26.4 memberi delay 0, 60, 120 ms. Firefox 146 belum kenal, jadi baris kedua dibuang saat dibaca dan baris cadangan yang berlaku. Tanpa baris cadangan, semua kartunya 0 ms. Tanpa - 1, Kamar Teras ikut menunggu 60 ms. Satu lagi yang kami temukan: baris sibling-index() jangan memakai var(). Deklarasi yang berisi var() gak diperiksa saat dibaca, jadi gak dibuang; di Firefox 146 dia baru gagal belakangan, dan delay-nya jadi 0, bukan nilai cadangannya.

Stagger dengan Web Animations API

Di JavaScript, delay-nya opsi delay biasa:

const cards = document.querySelectorAll('.room');
const anims = [...cards].map((card, i) =>
  card.animate(
    [{ opacity: 0, transform: 'translateY(16px)' }, { opacity: 1, transform: 'none' }],
    { duration: 300, delay: i * 60, easing: 'ease-out', fill: 'backwards' }, // backwards = tersembunyi selama delay
  ),
);

Sequence dengan promise: await animation.finished

Setiap objek Animation punya finished, sebuah Promise yang selesai saat animasinya selesai. Dengan await, langkah berikutnya menunggu:

async function tampilkanKamar() {
  const anims = masukkanKartu();                          // tiga kartu, stagger 60 ms
  await Promise.all(anims.map((a) => a.finished));        // tunggu yang terakhir: 420 ms
  await toast.animate(
    [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'none' }],
    { duration: 200, easing: 'ease-out' },
  ).finished;                                             // toast: 420 sampai 620 ms
}

Berapa stagger yang terlalu banyak

Panduan Material Design generasi pertama (m1.material.io, halaman Choreography) menulis: "Do not wait for each item to fully animate before introducing the next item." dan "Begin each item's staggered entrance no more than 20ms apart." Nielsen Norman Group menaruh kebanyakan animasi UI di 100 sampai 500 ms, dan Apple menyarankan "Let people cancel motion": jangan membuat orang menunggu.

Hitung totalnya sebelum memilih jeda. Tiga kartu × 60 ms selesai di 420 ms: aman. Dua belas hasil pencarian × 60 ms: kartu terakhir baru mulai di 660 ms dan selesai di 960 ms, hampir dua kali 500 ms. Batasi total jedanya:

const each = Math.min(60, 200 / (cards.length - 1)); // 12 kartu: 18,2 ms; selesai di 500 ms

GSAP punya opsi untuk ini: stagger: { amount: 0.2 } membagi 0,2 detik ke semua elemen. Saat "Kurangi gerakan" menyala, buang geser dan stagger-nya: semua kartu fade 150 ms bersamaan.

⚠️ Jebakan yang sering ketemu

TL;DR: Stagger = animasi sama, mulai bergiliran; delay ke-i = delay awal + jarak × jeda, dengan jarak i ("start"), (jumlah - 1) - i ("end"), atau Math.abs(i - (jumlah - 1) / 2) ("center"). Tiga kartu 300 ms berselang 60 ms: Kamar Kebun mulai 120 ms, semua selesai 420 ms. CSS: animation-delay: calc(var(--i) * 60ms) dengan both; sibling-index() mulai dari 1 (calc((sibling-index() - 1) * 60ms)), ada di Chrome 138, Safari 26.2, Firefox 154, dengan baris cadangan --i. WAAPI: delay: i * 60 dengan fill: 'backwards'. Sequence: await Promise.all(anims.map((a) => a.finished)) lalu toast (420 sampai 620 ms); finished ditolak saat dibatalkan. GSAP stagger: 0.15: elemen ke-5 mulai 0,6 detik; Motion 12: delayChildren: stagger(0.06) menggantikan staggerChildren. Jangan sampai urutannya menyeret: Material generasi pertama menulis jeda paling banyak 20 ms, NN/g 100 sampai 500 ms; 12 kartu × 60 ms = 960 ms, jadi batasi total jeda (Math.min(60, 200 / (n - 1)) atau GSAP amount).

Yang akan kamu pelajari