Framer Motion Basics — Web Animation

Framer Motion Basics Framer Motion (sekarang diimpor dari motion/react) adalah library animasi untuk React yang menyediakan API deklaratif dan powerful…

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. Saat shown jadi false, 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>

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:

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>

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>

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

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".

Yang akan kamu pelajari