Motion for React (dulu bernama Framer Motion) adalah library animasi untuk React: kamu menulis keadaan yang kamu mau di JSX, Motion yang mengurus perjalanannya. Bayangin pesan ojek online. Kamu cuma mengisi titik jemput dan titik tujuan; rute, kecepatan, dan kapan belok diurus sopirnya. Di Motion, titik jemput itu initial, tujuannya animate, dan gaya menyetirnya transition.
🧪 Tebakan cepat: Toast "Pesanan terkirim" ditulis
{shown && <motion.div exit={{ opacity: 0 }} />}, tanpa pembungkus apa pun. Saatshownjadifalse, apakah toast-nya memudar dulu selama 150 ms? Jawabannya ada di bagian "AnimatePresence: animasi keluar".
Nama, paket, dan versi
Framer Motion lahir di perusahaan Framer. Pada 12 November 2024 dia jadi proyek independen dengan nama Motion, paketnya motion, dan versi React-nya diimpor dari motion/react:
npm install motion
import { motion, AnimatePresence, stagger, useReducedMotion } from 'motion/react';
Paket lama framer-motion masih diterbitkan dengan nomor versi yang sama (paket motion sendiri memakainya di dalam), tapi kode baru sebaiknya memakai motion/react. Per 11 Oktober 2026 versi terbarunya 14.1.0; aplikasi BelajarWeb.dev memakai 12.38. Dari 12 ke 14 hampir gak ada yang berubah untuk kode biasa: versi 13 cuma menghapus dependensi opsional @emotion/is-prop-valid (penting kalau kamu memakai styled-components), versi 14 gak punya breaking change.
motion.div: initial dan animate
motion.div adalah <div> biasa yang bisa beranimasi. Ada versinya untuk tiap tag HTML: motion.li, motion.button, motion.section.
<motion.li
initial={{ opacity: 0, y: 16 }} // keadaan saat pertama dipasang
animate={{ opacity: 1, y: 0 }} // keadaan tujuan
transition={{ duration: 0.3, ease: 'easeOut' }}
>
Kamar Teras · Rp 350.000 / malam
</motion.li>
initial: keadaan awal saat komponen pertama kali dipasang (mount). Ini prop yang hilang di kuis "fade-in":initial={{ opacity: 0 }}laluanimate={{ opacity: 1 }}.initial={false}berarti langsung mulai di keadaananimatetanpa animasi masuk.animate: tujuannya. Setiap kali nilainya berubah karena state, Motion menganimasikan dari posisi sekarang ke tujuan baru.y: 16artinyatranslateY(16px). Motion punya nama pendek untuk transform:x,y,scale,rotate.durationdi Motion ditulis dalam detik:0.3= 300 ms. Ini kebalikan dari Web Animations API yang memakai milidetik.
Tombol "Pesan kamar" yang mengecil saat ditekan cukup satu prop: whileTap={{ scale: 0.96 }} dengan transition={{ duration: 0.1, ease: 'easeOut' }}. Ada juga whileHover, whileFocus, dan whileInView.
transition: durasi atau pegas
Kalau kamu gak menulis transition, Motion memilih sendiri. Kami baca sumber Motion 12.38: x, y, rotate memakai pegas (stiffness 500, damping 25), scale pegas yang lebih kaku (stiffness 550, damping 30), sedangkan opacity dan warna memakai kurva biasa 0,3 detik.
Ada dua jenis transition:
- tween: durasi dan easing, seperti CSS.
{ duration: 0.3, ease: 'easeOut' }. - spring (pegas):
{ type: 'spring', stiffness: 300, damping: 30, mass: 1 }. Gak ada durasi yang kamu tulis. Pegas ditarik ke tujuan, memantul, dan berhenti saat sudah diam.
Tiga angka pegas: stiffness (kekakuan, makin besar makin cepat menarik), damping (redaman, makin besar makin sedikit memantul), mass (massa, makin besar makin berat dan lambat). Durasinya muncul dari ketiganya. Kami menjalankan fungsi spring milik Motion untuk kartu yang naik dari y: 16 ke 0, sampai pegasnya dianggap diam (sisa jarak di bawah 0,5 px):
| transition | Diam setelah | Melewati tujuan |
|---|---|---|
bawaan untuk y: stiffness 500, damping 25 |
287 ms | 1,9 px |
type: 'spring' saja (100, 10, mass 1) |
687 ms | 2,6 px |
| stiffness 300, damping 30 | 315 ms | 0,03 px |
| stiffness 300, damping 10 | 741 ms | 6,2 px |
| stiffness 300, damping 30, mass 2 | 580 ms | 1,4 px |
Turunkan damping dari 30 ke 10: kartunya memantul 6,2 px dan baru diam setelah 741 ms. Gandakan mass: 315 ms jadi 580 ms. Pegas enak untuk gerak yang bisa disela (ditarik, dilepas, diubah tujuannya di tengah jalan) karena kecepatannya terbawa. Untuk fade, tween lebih bisa ditebak.
variants dan stagger kartu kamar
Tiga kartu (Kamar Teras, Loteng, Kebun) masuk bergantian, berjarak 60 ms. Daripada menulis delay di tiap kartu, beri nama pada keadaannya:
const daftar = {
hidden: {},
visible: { transition: { delayChildren: stagger(0.06) } }, // anak ke-n mulai n × 60 ms
};
const kartu = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: 'easeOut' } },
};
<motion.ul variants={daftar} initial="hidden" animate="visible">
{rooms.map((r) => (
<motion.li key={r.id} variants={kartu}>{r.nama}</motion.li>
))}
</motion.ul>
variantsadalah object berisi keadaan bernama.initial="hidden"dananimate="visible"cukup ditulis di induk; anak yang punyavariantsikut memakai nama yang sama.delayChildren: stagger(0.06)di induk menunda anak pertama 0 ms, kedua 60 ms, ketiga 120 ms. Jadi Kamar Teras selesai di 300 ms, Loteng 360 ms, Kebun 420 ms.- Kode lama menulis
staggerChildren: 0.06. Masih jalan, tapi di Motion 12 sudah ditandai deprecated; penggantinyadelayChildren: stagger(0.06).
AnimatePresence: animasi keluar
React menghapus elemen dari DOM begitu kondisinya false. Gak ada waktu tersisa untuk memainkan apa pun. Itu jawaban tebakannya: tanpa pembungkus, toast-nya langsung hilang, prop exit gak pernah dijalankan.
AnimatePresence menahan elemen yang dilepas sampai animasi exit-nya selesai, baru menghapusnya:
<AnimatePresence>
{shown && (
<motion.div
key="toast"
role="status"
className="toast"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.2, ease: 'easeOut' } }}
exit={{ opacity: 0, y: 16, transition: { duration: 0.15, ease: 'easeIn' } }}
>
Pesanan terkirim. Bu Sri akan konfirmasi lewat WhatsApp.
</motion.div>
)}
</AnimatePresence>
AnimatePresenceharus tetap terpasang; yang muncul dan hilang adalah anaknya.- Anak langsungnya perlu
keyyang unik, supaya Motion tahu elemen mana yang sedang pergi. <AnimatePresence mode="wait">menunggu yang lama selesai keluar sebelum yang baru masuk;onExitCompletedipanggil setelah semuanya pergi.
Di balik layar: WAAPI atau JavaScript
Motion memakai Web Animations API kalau bisa. Di Motion 12.38 daftarnya opacity, transform, filter, dan clipPath, selama kamu gak memasang onUpdate. Animasi itu bisa berjalan di compositor seperti CSS (pelajaran 5). Pegas pun dikirim ke WAAPI: Motion menghitung kurvanya lalu menuliskannya sebagai durasi tetap plus easing linear(...) (Chrome 113, Firefox 112, Safari 17.2). Kurva itu dihitung untuk jarak 0 sampai 100 dan dibulatkan ke 50 ms, jadi pegas 300/30 yang sama berjalan 500 ms di jalur ini.
Tapi x, y, scale, dan rotate yang ditulis terpisah gak lewat WAAPI. Motion menghitungnya sendiri di setiap frame lewat requestAnimationFrame, di main thread, lalu menulisnya ke style.transform. Kami ukur di Chromium 147: motion.div yang turun 300 px dalam 1 detik, lalu main thread diblokir 500 ms.
| Ditulis sebagai | Animasi WAAPI di elemen | Frame tergambar selama diblokir |
|---|---|---|
animate={{ y: 300 }} |
0 | 0 sampai 2 |
animate={{ transform: 'translateY(300px)' }} |
1 | 59 sampai 60 |
Versi y diam lalu meloncat, persis seperti hasil rAF di pelajaran 6. Di Lab-nya, kartu yang masuk dengan y punya satu animasi WAAPI (cuma opacity), yang masuk dengan transform punya dua.
Untuk animasi yang harus tetap mulus saat halaman sibuk, tulis transform dan opacity langsung.
useReducedMotion
Motion gak otomatis menghormati setelan "Kurangi gerakan". Bawaan MotionConfig adalah reducedMotion: "never". Ada dua cara:
const reduce = useReducedMotion(); // true kalau sistem minta gerakan dikurangi
const kartu = {
hidden: { opacity: 0, y: reduce ? 0 : 16 },
visible: { opacity: 1, y: 0, transition: { duration: reduce ? 0.15 : 0.3 } },
};
Atau bungkus aplikasi dengan <MotionConfig reducedMotion="user">: animasi x, y, scale, rotate, ukuran, dan posisi langsung lompat ke tujuan, sedangkan opacity tetap memudar. Itu sama dengan aturan Homestay Kabut: tanpa gerak, cukup fade 150 ms.
⚠️ Jebakan yang sering ketemu
exittanpaAnimatePresence. Elemennya hilang seketika. Bungkus kondisinya, dan beri anaknyakey.- Durasi ditulis milidetik.
duration: 300di Motion berarti 300 detik, lima menit. Tulis0.3. - Mengira
yjalan di compositor.x,y,scaleyang terpisah dihitung di main thread. Kalau halaman sering sibuk, pakaitransform. - Lupa "Kurangi gerakan". Bawaannya
reducedMotion: "never". PakaiuseReducedMotion()atauMotionConfig reducedMotion="user". initialdananimatedi setiap anak variants. Cukup di induk; anak cukupvariants. Kalau anak menulis sendiri, stagger induknya gak berlaku untuk dia.
TL;DR: Motion (dulu Framer Motion, independen sejak 12 November 2024, npm install motion, impor dari motion/react, terbaru 14.1.0) menganimasikan keadaan di JSX: initial keadaan awal saat mount, animate tujuan, transition caranya, dengan durasi dalam detik. Tween = duration + ease; spring = stiffness, damping, mass, dan durasinya muncul dari situ (300/30/1 diam di 315 ms, damping 10 memantul 6,2 px dan diam di 741 ms). variants memberi nama keadaan; delayChildren: stagger(0.06) di induk membuat kartu masuk 0, 60, 120 ms (staggerChildren sudah deprecated). exit hanya jalan di dalam <AnimatePresence> dengan anak ber-key. Motion memakai WAAPI untuk opacity, transform, filter, clipPath (pegas jadi easing linear()), tapi x, y, scale terpisah dihitung di main thread: diblokir 500 ms, y cuma tergambar 0 sampai 2 frame, transform 59 sampai 60. Motion gak menghormati "Kurangi gerakan" kecuali kamu memakai useReducedMotion() atau MotionConfig reducedMotion="user".