Animasi web adalah nilai CSS yang berubah sedikit demi sedikit, lalu digambar ulang berkali-kali dalam satu detik. Bayangin flipbook: tiap halamannya gambar diam, dan waktu dibalik cepat, gambarnya terlihat bergerak. Di browser, satu halaman itu namanya frame. Animasi terasa mulus kalau browser selalu sempat menyelesaikan frame berikutnya sebelum waktunya ditampilkan.
🧪 Tebakan cepat: Kartu Kamar Teras muncul dengan animasi 300 ms. Pak Hadi membukanya di HP 60 Hz, Bu Sri di HP 120 Hz. Di HP Bu Sri animasinya selesai lebih cepat? Dan berapa lama browser boleh menggambar satu frame di tiap HP? Jawabannya ada di bagian "Frame budget: 16,7 ms atau 8,3 ms".
Halaman yang akan kita gerakkan
Sepanjang jalur ini kita memakai halaman pesan Homestay Kabut, homestay kecil milik Bu Sri di Dieng, yang sudah kamu kenal dari jalur UI/UX. Isinya: kartu Kamar Teras (Rp 350.000 per malam), Kamar Loteng (Rp 275.000), dan Kamar Kebun (Rp 425.000); tombol 1, 2, 3 malam; total harga ("Total 2 malam: Rp 700.000"); tombol hijau "Pesan kamar, bayar di tempat"; dialog konfirmasi <dialog>; dan toast "Pesanan terkirim". Warnanya: hijau #15803d (hover #166534), teks #1f2937, teks kedua #475569, garis #e2e8f0.
Kapan dan kenapa sebuah momen perlu animasi sudah dibahas di pelajaran Microinteractions & Animation di jalur UI/UX. Jalur ini membahas caranya, sampai ke angka. Ini semua animasi di halaman itu:
| Momen | Yang bergerak | Durasi | Easing |
|---|---|---|---|
| Tombol malam ditekan | transform: scale(0.96) |
100 ms | ease-out |
| Tombol malam terpilih | background-color jadi #15803d |
150 ms | ease-out |
| Total harga berganti | angka baru naik 4 px sambil muncul | 200 ms | ease-out |
| Kartu kamar masuk | opacity 0 ke 1, naik 16 px |
300 ms | ease-out |
| Dialog konfirmasi | buka: opacity 0 ke 1, scale(0.96) ke 1 |
buka 200 ms, tutup 150 ms | buka ease-out, tutup ease-in |
| Toast "Pesanan terkirim" | naik dari bawah, tampil 5 detik | masuk 200 ms, keluar 150 ms | masuk ease-out, keluar ease-in |
| Galeri foto kamar | geser satu foto | 300 ms | ease-in-out |
Lima keluarga animasi
Animasi yang sama, kartu Kamar Teras masuk (300 ms, ease-out, opacity 0 ke 1 sambil naik 16 px), bisa ditulis dengan lima cara:
/* 1. CSS transition: bergerak di antara dua state saat nilainya berubah */
.card { opacity: 0; transform: translateY(16px);
transition: opacity 300ms ease-out, transform 300ms ease-out; }
.card.in { opacity: 1; transform: none; }
/* 2. CSS animation: naskah @keyframes yang jalan sendiri begitu aturannya berlaku */
@keyframes card-in { from { opacity: 0; transform: translateY(16px); } }
.card { animation: card-in 300ms ease-out; }
// 3. Web Animations API: keyframes yang sama dari JavaScript, bisa di-pause dan di-reverse
const anim = card.animate(
[{ opacity: 0, transform: 'translateY(16px)' }, { opacity: 1, transform: 'none' }],
{ duration: 300, easing: 'ease-out' }
);
// 4. requestAnimationFrame: kamu sendiri yang menghitung nilai di tiap frame
const start = performance.now();
function frame(now) {
const t = Math.min((now - start) / 300, 1); // waktu, 0 sampai 1
const p = 1 - (1 - t) ** 2; // melambat di akhir, mirip ease-out
card.style.opacity = p;
card.style.transform = `translateY(${16 * (1 - p)}px)`;
if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// 5. Library (misalnya Motion): satu panggilan, di baliknya Web Animations API atau rAF
animate(card, { opacity: [0, 1], transform: ['translateY(16px)', 'none'] }, { duration: 0.3, ease: 'easeOut' });
| Keluarga | Cocok untuk | Di halaman Bu Sri |
|---|---|---|
| CSS transition | dua state yang dipicu perubahan: hover, focus, kelas, atribut | tombol malam, warna tombol Pesan |
CSS animation (@keyframes) |
lebih dari dua langkah, berulang, atau jalan sendiri saat muncul | toast, titik "Memesan...", kartu masuk |
| Web Animations API | butuh kontrol: pause, reverse, tahu kapan selesai, nilai dari data | dialog yang dibatalkan di tengah jalan |
requestAnimationFrame / canvas |
nilai dihitung sendiri tiap frame: fisika, angka berjalan, canvas | total harga yang menghitung naik |
| Library (Motion, GSAP, Lottie) | urutan panjang, gesture, animasi komponen React, file After Effects | galeri foto dengan geser jari |
Para pengajar sering meringkasnya jadi tiga pilar: CSS transition, CSS animation (@keyframes), dan JavaScript. Pilar ketiga berisi keluarga 3 sampai 5: Web Animations API sebagai API resmi browser, requestAnimationFrame, dan library yang dibangun di atas keduanya.
Frame budget: 16,7 ms atau 8,3 ms
Layar menampilkan gambar baru sejumlah kali per detik. Angka itu disebut refresh rate, satuannya Hz. Satu detik = 1.000 ms, jadi waktu untuk satu frame adalah 1.000 dibagi refresh rate:
| Layar | Waktu per frame | Kartu 300 ms jadi |
|---|---|---|
| 60 Hz | 1.000 / 60 = 16,7 ms | 18 frame |
| 90 Hz | 1.000 / 90 = 11,1 ms | 27 frame |
| 120 Hz | 1.000 / 120 = 8,3 ms | 36 frame |
Itu jawaban tebakannya: animasinya tetap 300 ms di kedua HP, karena durasi diukur dengan waktu, bukan dengan jumlah frame. HP Bu Sri menggambar 36 frame, jadi lebih halus, tapi tiap frame cuma punya 8,3 ms. Itu frame budget. Kalau kerja browser untuk satu frame lewat dari budget, frame itu terlewat (dropped frame) dan geraknya tersendat. Gerak yang tersendat ini disebut jank. Artikel "Rendering performance" di web.dev menghitung budget 60 Hz sebagai 16,66 ms, lalu menyarankan kerjamu selesai dalam sekitar 10 ms, karena browser juga butuh waktu untuk dirinya sendiri.
Lima langkah menggambar satu frame
Untuk tiap frame, browser bisa melewati lima langkah (web.dev menyebutnya pixel pipeline):
- JavaScript: kode yang mengubah sesuatu, misalnya menambah kelas
inke kartu. - Style: menghitung aturan CSS mana yang berlaku ke elemen mana.
- Layout: menghitung posisi dan ukuran tiap kotak. Kalau kartu bertambah tinggi, semua yang di bawahnya ikut dihitung ulang.
- Paint: mewarnai piksel: teks, warna latar, bayangan, ke dalam satu atau beberapa lapisan (layer).
- Composite: menyusun layer-layer itu jadi satu gambar di layar.
Properti yang kamu animasikan menentukan langkah mana yang diulang di setiap frame:
| Properti | Diulang tiap frame |
|---|---|
top, left, width, height, margin |
Style, Layout, Paint, Composite |
box-shadow, color, border-color |
Style, Paint, Composite |
transform, opacity |
Composite saja |
Kami mengukurnya dengan Performance trace di Chromium 147: kartu naik 16 px selama 300 ms. Lewat top, layout dihitung 35 sampai 37 kali dan paint 70 sampai 72 kali. Lewat transform, layout 1 kali dan paint 2 kali, keduanya di awal saja. Detailnya ada di pelajaran Transform & Composite.
Langkah composite dikerjakan di thread terpisah, compositor thread. Animasi transform dan opacity dari CSS atau Web Animations API bisa jalan sepenuhnya di sana, tanpa menunggu main thread (thread tempat JavaScript dan layout jalan). Kami membuat main thread sibuk selama 500 ms di tengah animasi di Chromium 147: CSS transition tetap menggambar 61 frame, Web Animations API 60 frame, sedangkan versi requestAnimationFrame cuma 1 frame. Geraknya membeku, lalu melompat.
CSS atau JavaScript?
Mulai dari CSS. Pindah ke JavaScript kalau animasinya butuh salah satu dari ini:
- Logika dinamis: nilainya baru diketahui saat jalan, misalnya posisi tujuan dihitung dari data, atau total harga yang menghitung naik dari Rp 700.000 ke Rp 1.050.000.
- Interaksi yang rumit: mengikuti jari yang menggeser galeri foto.
- Kontrol playback:
pause(),reverse(), lompat ke waktu tertentu, atau menungguanim.finishedsebelum langkah berikutnya.
Alasannya bukan soal thread. Justru requestAnimationFrame selalu jalan di main thread, jadi ikut tersendat kalau main thread sibuk, seperti hasil ukur di atas.
prefers-reduced-motion sejak hari pertama
Ada orang yang pusing atau mual melihat gerakan di layar. Mereka menyalakan "Kurangi gerakan" di pengaturan HP atau komputernya, dan browser meneruskannya sebagai media query prefers-reduced-motion: reduce (didukung sejak Chrome 74, Firefox 63, Safari 10.1, menurut MDN). Hormati sejak animasi pertama: buang gerakannya, sisakan fade singkat.
@media (prefers-reduced-motion: reduce) {
.card { transform: none; transition: opacity 150ms linear; }
}
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
card.animate(
reduce ? [{ opacity: 0 }, { opacity: 1 }]
: [{ opacity: 0, transform: 'translateY(16px)' }, { opacity: 1, transform: 'none' }],
{ duration: reduce ? 150 : 300, easing: 'ease-out' }
);
- Di CSS, aturan di dalam
@mediamenimpa gerakannya: kartu gak naik 16 px, cuma memudar 150 ms. - Di JavaScript,
matchMedia(...).matchesbernilaitruekalau pengaturan itu menyala. - Jangan pakai
transition: nonesebagai gantinya. Kenapa itu bisa merusak halaman dijelaskan di pelajaran Microinteractions & Animation.
⚠️ Jebakan yang sering ketemu
- Menghitung gerak per frame, bukan per waktu.
x += 2di tiap frame menempuh 300 px dalam 2,5 detik di layar 60 Hz, tapi 1,25 detik di layar 120 Hz. Hitung dari waktu yang sudah lewat, seperti(now - start) / 300. - Menganimasikan
top,width, ataumargin. Layout dihitung ulang di setiap frame. Pakaitransformuntuk gerak dan ukuran,opacityuntuk muncul dan hilang. - Langsung pakai JavaScript. Hover, focus, dan muncul sederhana cukup dengan CSS: lebih pendek, dan bisa jalan di compositor thread.
- Lupa
prefers-reduced-motion. Sisakan fade singkat untuk yang menyalakannya.
TL;DR: Animasi web adalah nilai CSS yang berubah sedikit demi sedikit, digambar per frame. Lima keluarga: CSS transition (dua state), CSS animation @keyframes (naskah bertahap), Web Animations API (keyframes dari JS dengan pause dan reverse), requestAnimationFrame (hitung sendiri tiap frame), dan library. Ringkasnya tiga pilar: transition, @keyframes, dan JavaScript. Frame budget = 1.000 ms dibagi refresh rate: 16,7 ms di 60 Hz, 8,3 ms di 120 Hz; animasi 300 ms tetap 300 ms, cuma jumlah frame-nya yang beda (18 atau 36). Tiap frame melewati JavaScript, Style, Layout, Paint, Composite; transform dan opacity cukup sampai composite dan bisa jalan di compositor thread walau main thread sibuk, sedangkan rAF ikut tersendat. Pakai JavaScript kalau butuh logika dinamis, interaksi rumit, atau kontrol playback. Hormati prefers-reduced-motion sejak awal: buang gerakan, sisakan fade.