Animation library Framer Motion là default cho React khi cần motion phức tạp hơn CSS transition. Bài này code mượt 60fps, accessibility-aware, production-ready.
Animation cơ bản
npm install framer-motion
import { motion } from 'framer-motion'
function FadeIn() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
>
Content
</motion.div>
)
}
motion.div = wrapper động cho thẻ div thường. initial/animate/exit là 3 state. transition control thời gian + easing.
Variants — gọn + reuse
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
hover: { scale: 1.02, transition: { duration: 0.2 } },
}
function Card() {
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
>
...
</motion.div>
)
}
Stagger children
const container = {
hidden: {},
visible: {
transition: { staggerChildren: 0.1 },
},
}
const item = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map((it, i) => (
<motion.li key={it.id} variants={item}>{it.name}</motion.li>
))}
</motion.ul>
Mỗi item delay 0.1s sau item trước → cascade effect đẹp.
AnimatePresence — exit animation
import { AnimatePresence, motion } from 'framer-motion'
function Modal({ isOpen, onClose }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: 'spring', damping: 25 }}
onClick={e => e.stopPropagation()}
>
Modal content
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
Layout animation
// Auto animate khi layout thay đổi
function Toggle() {
const [open, setOpen] = useState(false)
return (
<motion.div layout className="rounded-xl border p-4">
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <motion.p layout>Extra content...</motion.p>}
</motion.div>
)
}
FM tự đo position trước/sau, animate FLIP — không cần khai báo cụ thể.
Shared element transition
// Image trong list
<motion.img layoutId={`photo-${id}`} src={url} className="w-32 h-32" />
// Image trong detail page
<motion.img layoutId={`photo-${id}`} src={url} className="w-full h-auto" />
Khi navigate, FM detect cùng layoutId → animate smooth từ small image (list) sang large image (detail).
Gesture — drag, hover, tap
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
drag
dragConstraints={{ left: 0, right: 200, top: 0, bottom: 200 }}
dragElastic={0.2}
>
Drag me
</motion.div>
Scroll-triggered animation
import { motion, useScroll, useTransform } from 'framer-motion'
function Parallax() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -300])
return (
<motion.div style={{ y }}>
Element parallax
</motion.div>
)
}
// Hoặc whileInView
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true, amount: 0.3 }}
>
Animate khi scroll vào view
</motion.div>
Performance pitfall
// ❌ Animate layout property — tốn CPU, layout thrashing
animate={{ width: 300, height: 200, top: 50 }}
// ✓ Animate transform + opacity — GPU, 60fps
animate={{ scale: 1.5, x: 50, opacity: 0.8 }}
Quy tắc: chỉ animate transform (translate, scale, rotate) và opacity. Kết hợp layout prop khi cần auto animate.
Accessibility — prefers-reduced-motion
import { MotionConfig } from 'framer-motion'
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
FM auto disable transform/animation khi user bật "Reduce Motion" trong OS. Anh không phải code thêm.
Kết luận
Animation library Framer Motion mạnh khi anh cần nhiều hơn CSS transition — gesture, layout, exit animation, scroll. Bundle 30KB là hợp lý cho UX mượt. Nhớ animate transform/opacity, respect prefers-reduced-motion, và dùng layout prop khi structure thay đổi. Tham khảo Web Vitals để đảm bảo animation không phá INP.