Bỏ qua đến nội dung chính
Framer MotionanimationReactUXfrontend

Animation Library: Framer Motion Cho UX Mượt Mà

Animation library Framer Motion: variants, gesture, layout animation, scroll trigger. Code mẫu mượt 60fps, accessibility, performance tip.

Xuất bản 7 phút đọc

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.

Zalo