Framer Motion Complete Guide | React Animations Made Simple

Framer Motion Complete Guide | React Animations Made Simple

이 글의 핵심

Framer Motion is the go-to animation library for React — declarative, GPU-accelerated, and powerful enough for complex page transitions and gesture-driven UIs. This guide covers everything from basic animations to advanced layout animations.

What This Guide Covers

Framer Motion makes React animations declarative and composable. Drop in a motion component, add animate props, and it handles the rest — spring physics, gesture detection, and smooth layout transitions.

Real-world insight: Replacing CSS transitions with Framer Motion variants cut animation code by 60% and finally made exit animations (modal close, toast dismiss) work correctly without hacks.


Installation

npm install framer-motion

1. Basic Animation

Replace any HTML element with its motion equivalent:

import { motion } from 'framer-motion'

// Animate on mount
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.4 }}
>
  Hello World
</motion.div>

// Any HTML element
<motion.h1 animate={{ scale: 1.1 }} />
<motion.button whileHover={{ scale: 1.05 }} />
<motion.img animate={{ rotate: 360 }} />

2. Transition Configuration

// Spring physics (natural feel)
<motion.div
  animate={{ x: 100 }}
  transition={{ type: 'spring', stiffness: 300, damping: 20 }}
/>

// Tween (controlled timing)
<motion.div
  animate={{ opacity: 1 }}
  transition={{ type: 'tween', duration: 0.3, ease: 'easeOut' }}
/>

// Delay and repeat
<motion.div
  animate={{ y: [0, -10, 0] }}  // keyframes
  transition={{ repeat: Infinity, duration: 1, ease: 'easeInOut' }}
/>

// Stagger children (via parent)
<motion.ul
  initial="hidden"
  animate="visible"
  variants={{
    visible: { transition: { staggerChildren: 0.1 } }
  }}
>
  {items.map(item => (
    <motion.li
      key={item.id}
      variants={{
        hidden: { opacity: 0, x: -20 },
        visible: { opacity: 1, x: 0 }
      }}
    />
  ))}
</motion.ul>

3. Variants

Variants define named animation states and let you propagate animation to children:

const cardVariants = {
  hidden: { opacity: 0, y: 30 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.4, ease: 'easeOut' }
  },
  hover: { scale: 1.02, boxShadow: '0 10px 30px rgba(0,0,0,0.1)' },
  tap: { scale: 0.98 },
}

<motion.div
  variants={cardVariants}
  initial="hidden"
  animate="visible"
  whileHover="hover"
  whileTap="tap"
>
  <h2>Card Title</h2>
</motion.div>

4. Gesture Animations

// Hover and tap
<motion.button
  whileHover={{ scale: 1.05, backgroundColor: '#3b82f6' }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
  Click me
</motion.button>

// Drag
<motion.div
  drag
  dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
  dragElastic={0.2}
  whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
  style={{ cursor: 'grab', width: 100, height: 100, background: '#3b82f6' }}
/>

// Drag with snap back
<motion.div
  drag
  dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
  // dragConstraints = same as origin → snaps back
/>

5. AnimatePresence (Exit Animations)

import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'

function Modal({ isOpen, onClose }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          {/* Backdrop */}
          <motion.div
            key="backdrop"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={onClose}
            style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)' }}
          />
          {/* Modal */}
          <motion.div
            key="modal"
            initial={{ opacity: 0, scale: 0.9, y: 20 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.9, y: 20 }}
            transition={{ type: 'spring', duration: 0.3 }}
            style={{ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
          >
            <p>Modal content</p>
            <button onClick={onClose}>Close</button>
          </motion.div>
        </>
      )}
    </AnimatePresence>
  )
}
// Toast notifications
function ToastList({ toasts }) {
  return (
    <div style={{ position: 'fixed', bottom: 20, right: 20 }}>
      <AnimatePresence>
        {toasts.map(toast => (
          <motion.div
            key={toast.id}
            initial={{ opacity: 0, x: 100, scale: 0.9 }}
            animate={{ opacity: 1, x: 0, scale: 1 }}
            exit={{ opacity: 0, x: 100 }}
            layout  // smooth reorder when toasts are added/removed
          >
            {toast.message}
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  )
}

6. Page Transitions (Next.js)

// components/PageTransition.jsx
import { motion, AnimatePresence } from 'framer-motion'
import { useRouter } from 'next/router'

const pageVariants = {
  initial: { opacity: 0, x: -20 },
  animate: { opacity: 1, x: 0 },
  exit: { opacity: 0, x: 20 },
}

export function PageTransition({ children }) {
  const { pathname } = useRouter()
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        variants={pageVariants}
        initial="initial"
        animate="animate"
        exit="exit"
        transition={{ duration: 0.2 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

7. Layout Animations

Automatically animate size and position changes — no manual calculations:

import { motion, LayoutGroup } from 'framer-motion'
import { useState } from 'react'

function Accordion() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <motion.div layout onClick={() => setIsOpen(!isOpen)} style={{ overflow: 'hidden' }}>
      <motion.h3 layout>Click to expand</motion.h3>
      {isOpen && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
        >
          Hidden content revealed with smooth height animation
        </motion.p>
      )}
    </motion.div>
  )
}

// Shared layout (element morphs between positions)
function TabList() {
  const [activeTab, setActiveTab] = useState('home')
  const tabs = ['home', 'about', 'contact']

  return (
    <LayoutGroup>
      <div style={{ display: 'flex', gap: 8 }}>
        {tabs.map(tab => (
          <button key={tab} onClick={() => setActiveTab(tab)} style={{ position: 'relative' }}>
            {tab}
            {activeTab === tab && (
              <motion.div
                layoutId="active-tab"  // same layoutId = shared layout animation
                style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'blue' }}
              />
            )}
          </button>
        ))}
      </div>
    </LayoutGroup>
  )
}

8. useAnimation Hook

Control animations imperatively:

import { motion, useAnimation } from 'framer-motion'
import { useEffect } from 'react'

function ShakeInput({ hasError }) {
  const controls = useAnimation()

  useEffect(() => {
    if (hasError) {
      controls.start({
        x: [0, -10, 10, -10, 10, 0],
        transition: { duration: 0.4 }
      })
    }
  }, [hasError])

  return (
    <motion.input
      animate={controls}
      style={{ borderColor: hasError ? 'red' : 'gray' }}
    />
  )
}

9. Scroll Animations

import { motion, useScroll, useTransform } from 'framer-motion'
import { useRef } from 'react'

// Scroll progress
function HeroSection() {
  const { scrollY } = useScroll()
  const opacity = useTransform(scrollY, [0, 300], [1, 0])
  const y = useTransform(scrollY, [0, 300], [0, -100])

  return (
    <motion.section style={{ opacity, y }}>
      <h1>Parallax Hero</h1>
    </motion.section>
  )
}

// Animate when element enters viewport
function FadeInSection({ children }) {
  const ref = useRef(null)

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 30 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: '-100px' }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  )
}

Key Takeaways

ConceptUse for
initial / animateMount animations
exit + AnimatePresenceUnmount animations (modals, toasts)
whileHover / whileTapGesture feedback
variantsNamed states, propagate to children, stagger
layoutAutomatic size/position change animation
layoutIdShared layout — element morphs between components
whileInViewAnimate when element enters viewport
useScroll + useTransformScroll-driven animations

Framer Motion’s declarative API makes animations feel like a design system — define states (hidden, visible, hover) and let the library handle interpolation. Start with initial/animate/exit, add variants for reuse, and reach for layout and layoutId when you need elements to animate smoothly between states.