Transition CSS itu cocok kalau kamu punya kondisi “sebelum” dan “sesudah” yang jelas — misalnya tombol yang warnanya satu saat diam, lalu berubah ketika di-hover. Tapi bagaimana dengan gerakan yang tidak bergantung pada perubahan kondisi? Spinner yang berputar terus-menerus. Titik notifikasi yang berdenyut untuk menarik perhatian. Bilah loading yang merayap melintasi layar begitu halaman dibuka. Transition tidak bisa melakukan itu semua, karena tidak ada kondisi kedua untuk dituju.
Di sinilah animasi CSS masuk. Alih-alih mendeskripsikan dua titik ujung, kamu mendeskripsikan satu garis waktu utuh — rangkaian keyframe yang diputar oleh browser dari awal sampai akhir. Kamu bisa mengulanginya, membalik arahnya, menjedanya, menjalankan beberapa sekaligus, dan memicunya tanpa interaksi pengguna sama sekali. Dan sama seperti transition, ini murni CSS: tanpa JavaScript, tanpa library animasi, cukup beberapa baris aturan. Mari kita mulai dari animasi paling sederhana sampai pola-pola nyata yang benar-benar kamu pakai di proyek.
Transition vs. animasi: mana yang kamu butuhkan?
Sebelum masuk ke sintaks, ada baiknya kamu paham dulu kapan memakai yang mana, karena keduanya memang sedikit tumpang tindih.
Transition menganimasikan perpindahan antara dua kondisi dan butuh pemicu — biasanya :hover, :focus, atau sebuah class yang di-toggle lewat JavaScript. Dia berpindah dari posisi sekarang ke posisi yang kamu tentukan, sekali jalan.
Animasi memutar garis waktu yang sudah kamu rancang sendiri, dan dia bisa mulai sendiri, berulang selamanya, serta melewati sebanyak apa pun tahapan di antaranya. Tanpa perlu pemicu maupun perubahan kondisi.
Patokan cepat
Kalau gerakannya merupakan reaksi atas sesuatu yang dilakukan pengguna (hover, klik, fokus), transition biasanya lebih sederhana dan memang pilihan yang tepat. Kalau gerakannya jalan sendiri, berulang, atau melewati beberapa tahap, kamu butuh animasi. Banyak antarmuka yang rapi memakai keduanya berdampingan.
Langkah satu: mendefinisikan aturan @keyframes
Animasi CSS punya dua bagian. Pertama, kamu mendefinisikan gerakannya lewat aturan @keyframes, memberinya nama dan menjelaskan seperti apa tampilan elemen pada setiap titik di garis waktu. Kedua, kamu menerapkannya ke sebuah elemen lewat property animation. Kita mulai dari definisinya:
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
Aturan ini bilang: di awal (from), elemen benar-benar transparan; di akhir (to), elemen jadi sepenuhnya tampak. Nama fade-in itu kamu yang buat sendiri — sebentar lagi kamu akan memakainya untuk menempelkan animasi ini ke sebuah elemen. from dan to cuma nama ramah untuk 0% dan 100% dari garis waktu.
Berdiri sendiri, aturan @keyframes tidak melakukan apa-apa. Dia cuma resep yang tergeletak di rak. Belum ada yang bergerak sampai kamu benar-benar menerapkannya.
Langkah dua: menerapkannya lewat property animation
Untuk menjalankan animasinya, arahkan sebuah elemen ke animasi itu lewat namanya, lalu beri durasi:
.box {
animation-name: fade-in;
animation-duration: 1s;
}
Sekarang setiap elemen ber-class box akan memudar dari tak tampak ke tampak selama satu detik begitu dia muncul di halaman. Dua baris itu — animasi mana dan berapa lama — adalah modal paling minimal untuk memunculkan gerakan di layar. Sisanya tinggal penyetelan.
Pada praktiknya kamu hampir selalu akan memakai bentuk singkat animation daripada menulis tiap property satu per satu, tapi melihatnya terpisah dulu seperti ini bikin bentuk singkatnya jadi jauh lebih masuk akal nanti. Kita akan sampai ke situ.
Keyframe persentase: lebih dari dua tahap
from/to cukup untuk kasus sederhana, tapi kekuatan asli @keyframes ada pada kemampuannya mendefinisikan sebanyak apa pun tahap di antaranya memakai persentase. Ini contoh gerakan memantul:
@keyframes bounce {
0% { transform: translateY(0); }
50% { transform: translateY(-30px); }
100% { transform: translateY(0); }
}
Elemen mulai dari posisi normalnya, naik 30 piksel saat tepat di tengah, lalu turun lagi di akhir. Browser akan menginterpolasi gerakan di antara tiap titik secara mulus, jadi kamu dapat pantulan naik-turun yang rapi tanpa perlu menjabarkan setiap frame — kamu cukup menandai titik-titik baliknya saja.
Kamu bisa menambahkan sebanyak apa pun titik persentase sesuai kebutuhan. Mau efek goyang? Tentukan beberapa posisi kiri-kanan di 25%, 50%, dan 75%. Tiap persentase adalah pos pemeriksaan; browser yang mengisi gerakan di antaranya.
Animasikan transform dan opacity supaya mulus
Perhatikan, gerakan memantul tadi memindahkan elemen dengan transform: translateY(...), bukan mengubah top atau margin. Itu disengaja. Browser bisa menganimasikan transform dan opacity di GPU tanpa menata ulang tata letak halaman, jadi gerakannya tetap mulus. Sebaliknya, menganimasikan property tata letak seperti width, height, top, atau margin memaksa browser menghitung ulang posisi di tiap frame dan bisa bikin gerakan tersendat. Selama bisa, tetaplah pakai transform dan opacity.
Perulangan: animation-iteration-count
Secara bawaan, animasi diputar tepat sekali lalu berhenti. Supaya berulang, atur animation-iteration-count:
.spinner {
animation-name: spin;
animation-duration: 1s;
animation-iteration-count: infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
infinite adalah nilai yang paling sering kamu pakai — inilah yang membuat semua spinner loading di web berputar tanpa henti. Kamu juga bisa memberi angka biasa (animation-iteration-count: 3) untuk mengulang sejumlah tertentu, yang berguna misalnya untuk efek getar penarik perhatian yang jalan tiga kali lalu berhenti.
Durasi dan easing: animation-timing-function
Sama seperti transition, animasi punya kurva easing yang mengatur ritme gerakannya — apakah dia mulai pelan lalu mempercepat, atau bergerak dengan kecepatan tetap:
.box {
animation-name: fade-in;
animation-duration: 1s;
animation-timing-function: ease-in-out;
}
Nilai yang umum ada linear (kecepatan tetap — pilihan tepat untuk spinner, yang memang harus berputar rata), ease (bawaan — mulai dan berhenti dengan halus), ease-in, ease-out, dan ease-in-out. Untuk gerakan berulang yang seharusnya terasa mekanis dan stabil, pakai linear; untuk apa pun yang masuk atau mengendap, varian ease terasa lebih natural.
Delay, arah, dan fill: sentuhan akhir
Beberapa property lagi memberimu kendali yang lebih presisi atas perilaku animasi.
animation-delay menunggu dulu sebelum mulai — pas untuk membuat beberapa elemen tampil bergiliran, bukan serempak bersamaan:
.card:nth-child(1) { animation-delay: 0s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }
animation-direction mengatur arah pemutaran garis waktu. normal berarti dari awal ke akhir; reverse memutarnya mundur; dan alternate memutar maju, lalu mundur, lalu maju lagi — dan inilah yang membuat sebuah denyut terasa “bernapas” masuk-keluar dengan mulus, bukannya melompat balik ke awal di tiap putaran:
.pulse {
animation-name: grow;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes grow {
from { transform: scale(1); }
to { transform: scale(1.1); }
}
animation-fill-mode menentukan seperti apa tampilan elemen sebelum animasi mulai dan sesudah dia selesai. Secara bawaan, elemen akan kembali ke gaya non-animasinya begitu animasi berakhir. Atur animation-fill-mode: forwards, dan dia akan mempertahankan gaya dari keyframe terakhir — penting saat kamu menganimasikan sesuatu agar muncul dan ingin dia tetap terlihat, bukan lenyap lagi di akhir.
Bentuk singkat animation
Menulis enam baris animation-* terpisah cepat terasa melelahkan. Bentuk singkat animation merangkum semuanya jadi satu deklarasi:
.box {
animation: fade-in 1s ease-in-out 0.2s infinite alternate forwards;
}
Urutannya: nama, durasi, timing-function, delay, iteration-count, direction, lalu fill-mode. Kamu tidak harus menyertakan semuanya — yang wajib cuma durasi dan nama, sisanya kembali ke nilai bawaannya kalau kamu lewatkan. Spinner yang realistis cukup begini:
.spinner {
animation: spin 1s linear infinite;
}
Durasi datang sebelum delay
Bentuk singkat ini menerima dua nilai waktu: yang pertama selalu durasi, yang kedua delay. Jadi animation: spin 2s 1s berarti animasi 2 detik yang baru mulai setelah menunggu 1 detik — bukan sebaliknya. Kalau animasimu jalan dengan kecepatan yang salah, urutan dua angka yang tertukar ini sering jadi biang keroknya. Kalau ragu, tulis dulu animation-duration dan animation-delay terpisah sampai jalan benar, baru ringkas ke bentuk singkat.
Contoh nyata: spinner loading
Mari kita bangun animasi CSS yang paling sering muncul di seluruh web — sebuah spinner — dari nol:
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0; /* cincin abu-abu terang */
border-top-color: #00b8e6; /* satu sisi diberi warna */
border-radius: 50%; /* jadikan lingkaran */
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
<div class="spinner"></div>
Triknya cantik: sebuah lingkaran dengan border abu-abu, kecuali satu sisinya diberi warna. Putar terus seluruhnya dengan timing linear, dan matamu akan membaca busur berwarna itu seolah mengejar ekornya sendiri. Perhatikan keyframe-nya cukup punya to — saat kamu menghilangkan from, browser memakai kondisi elemen saat ini sebagai titik awal, jadi rotate(0deg) sudah dianggap ada. Itulah spinner yang sudah ribuan kali kamu lihat, hanya dalam lima baris CSS.
Contoh kedua: titik notifikasi yang berdenyut
Ini satu pola lagi yang bakal sering kamu pakai — sebuah titik yang berdenyut pelan untuk menarik perhatian, seperti penanda pesan belum dibaca:
.notification-dot {
width: 12px;
height: 12px;
background: #ff5252;
border-radius: 50%;
animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.6; }
}
Menggabungkan 0%, 100% dalam satu baris adalah jalan pintas yang rapi: artinya “jadilah seperti ini di awal sekaligus di akhir,” sehingga titiknya kembali ke ukuran semula di tiap putaran. Saat di tengah, dia membesar dan memudar sedikit, lalu balik lagi — denyut lembut yang terasa “bernapas”, cukup mencolok tapi tidak mengganggu.
Hormati pengguna yang lebih suka gerakan minimal
Tidak semua orang nyaman dengan gerakan di halaman — bagi sebagian orang, itu benar-benar bikin tidak nyaman atau pusing. Browser menyediakan pengaturan untuk ini, dan kamu sebaiknya menghormatinya. Media query prefers-reduced-motion membiarkan kamu menurunkan intensitas animasi bagi pengguna yang sudah meminta sistemnya untuk meminimalkan gerakan:
@media (prefers-reduced-motion: reduce) {
.spinner,
.pulse,
.notification-dot {
animation: none;
}
}
Ini syarat aksesibilitas, bukan sekadar pemanis
Menghormati prefers-reduced-motion bukan poles opsional — bagi pengguna dengan gangguan vestibular, gerakan yang tak diinginkan bisa memicu mual dan pusing. Setiap kali kamu menambahkan gerakan berulang, geseran, zoom, atau parallax, sertakan aturan prefers-reduced-motion: reduce yang mematikan atau meredamnya. Cuma blok CSS kecil, tapi membuat situsmu tetap bisa dipakai oleh orang yang tanpa itu terpaksa harus pergi.
Menjeda dan melanjutkan dengan animation-play-state
Satu property berguna terakhir: animation-play-state bisa menjeda (paused) animasi yang sedang jalan dan menjalankannya lagi (running). Ini cara bersih untuk menghentikan spinner saat di-hover, atau membekukan carousel selama pointer ada di atasnya:
.spinner:hover {
animation-play-state: paused;
}
Animasinya membeku persis di posisinya saat itu, lalu lanjut dari titik yang sama begitu pointer menjauh — tanpa lompatan, tanpa mengulang dari awal. Property ini juga cocok dipadukan dengan JavaScript: cukup toggle sebuah class, dan kamu bisa memulai serta menghentikan animasi CSS tanpa pernah membuat ulang animasinya.
Penutup
Animasi CSS membiarkanmu membangun gerakan yang jalan sendiri — berulang, bertahap, dan sepenuhnya tanpa JavaScript. Inilah yang sudah kamu kuasai:
@keyframes nama { ... }mendefinisikan garis waktu memakaifrom/toatau titik persentase;animation-name+animation-durationmenerapkannya.- Pakai keyframe persentase untuk gerakan bertahap, dan animasikan
transformsertaopacitydemi hasil yang mulus dan ramah GPU. animation-iteration-count: infinitemengulang selamanya;animation-timing-functionmengatur easing-nya (linearuntuk spinner).animation-delaymembuat tampil bergiliran,animation-direction: alternatebikin denyut “bernapas”, dananimation-fill-mode: forwardsmempertahankan kondisi akhir.- Bentuk singkat
animationmerangkum semuanya — ingat saja durasi datang sebelum delay. - Selalu sertakan aturan
prefers-reduced-motion, dan pakaianimation-play-stateuntuk menjeda lalu melanjutkan.
Dengan transition dan animasi sama-sama ada di kotak perkakasmu, kamu bisa membuat hampir semua antarmuka terasa hidup dan responsif tanpa menyentuh JavaScript. Berikutnya kita akan menengok shadow CSS — box shadow dan text shadow yang memberi kedalaman pada desainmu dan mengangkatnya dari permukaan halaman.