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; } }
calc(var(--i) * 60ms)mengubah angka urutan jadi waktu:--i: 2jadi 120 ms.both(fill mode) penting. Tanpa itu, Kamar Kebun tampil penuh selama 120 ms delay-nya, lalu tiba-tiba hilang dan baru masuk: kedipan.
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
}
Promise.allmenunggu semua, jadi toast mulai setelah Kamar Kebun mendarat di 420 ms dan selesai di 620 ms.- Animasi CSS juga punya
finished: ambil dengancard.getAnimations(). - Kalau animasinya dibatalkan (
cancel(), atau elemennya dihapus),finishedditolak denganAbortError. Bungkus dengantry/catchsupaya urutannya gak macet.
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
- Lupa
bothataufill: 'backwards'. Kartu terlihat selama delay, lalu berkedip hilang dan masuk lagi. sibling-index()dianggap mulai dari 0.calc(sibling-index() * 60ms)membuat Kamar Teras menunggu 60 ms. Kurangi 1, tulis baris cadangan--iuntuk Firefox lama, dan jangan pakaivar()di barissibling-index().- Sequence padahal maksudnya stagger. Menunggu tiap kartu selesai baru memulai yang berikut = 3 × 300 = 900 ms.
- Tengah dihitung dengan indeks yang dibulatkan. Untuk
"center"pakaiMath.abs(i - (jumlah - 1) / 2): dengan 3 item,["m", "n", "o"], delay awal 10, jeda 5 jadi 15, 10, 15. Delay awal tetap ditambahkan ke semuanya.
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).