React Framer Motion: Animations and Transitions Guide (2026)

Framer Motion is the production animation library for React. It handles enter/exit animations, layout transitions, drag gestures, scroll-driven effects and spring physics — all with a declarative API that stays in harmony with React's rendering model. This guide covers everything from basic motion components to advanced orchestration with variants.

Motion Components and Basic Animations

npm install framer-motion
import { motion } from 'framer-motion'

// motion.div = regular div + animation superpowers
function FadeIn() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}   // Start state
      animate={{ opacity: 1, y: 0 }}    // End state
      transition={{ duration: 0.4, ease: 'easeOut' }}
    >
      Hello, animated world!
    </motion.div>
  )
}

// Keyframes — array of values
<motion.div
  animate={{ x: [0, 100, 0] }}
  transition={{ duration: 2, repeat: Infinity }}
/>

// Animate on state change
function Toggle() {
  const [open, setOpen] = useState(false)
  return (
    <motion.div
      animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }}
      transition={{ duration: 0.3 }}
      style={{ overflow: 'hidden' }}
    >
      Content
    </motion.div>
  )
}

Variants and Orchestration

Variants let you define named animation states and propagate them to children — essential for staggered list animations:

import { motion } from 'framer-motion'

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,   // Each child animates 100ms after previous
      delayChildren: 0.2,     // Wait 200ms before starting children
    },
  },
}

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { type: 'spring', stiffness: 300, damping: 24 },
  },
}

function AnimatedList({ items }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map(item => (
        <motion.li key={item.id} variants={itemVariants}>
          {item.title}
        </motion.li>
      ))}
    </motion.ul>
  )
}
// Children inherit parent's initial/animate — no need to repeat them

AnimatePresence: Enter/Exit

AnimatePresence enables exit animations — components play their exit animation before unmounting:

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

// Modal with enter/exit
function Modal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          {/* Backdrop */}
          <motion.div
            key="backdrop"
            className="fixed inset-0 bg-black/50"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={onClose}
          />
          {/* Modal panel */}
          <motion.div
            key="modal"
            className="fixed inset-x-4 top-1/2 -translate-y-1/2 bg-white rounded-xl p-6"
            initial={{ opacity: 0, scale: 0.9, y: '-48%' }}
            animate={{ opacity: 1, scale: 1, y: '-50%' }}
            exit={{ opacity: 0, scale: 0.9, y: '-48%' }}
            transition={{ type: 'spring', stiffness: 400, damping: 30 }}
          >
            {children}
          </motion.div>
        </>
      )}
    </AnimatePresence>
  )
}

// Page transitions with React Router
function PageWrapper({ children }) {
  const location = useLocation()
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={location.pathname}
        initial={{ opacity: 0, x: 20 }}
        animate={{ opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: -20 }}
        transition={{ duration: 0.2 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

Layout Animations

The layout prop automatically animates when a component's size or position changes — no keyframes needed:

// Animates position/size changes automatically
function ExpandableCard({ title, content }) {
  const [expanded, setExpanded] = useState(false)
  return (
    <motion.div
      layout                          // Animates layout changes
      onClick={() => setExpanded(!expanded)}
      className="card cursor-pointer"
      style={{ borderRadius: 16 }}    // Must be inline for layout anim
    >
      <motion.h3 layout="position">{title}</motion.h3>
      {expanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          {content}
        </motion.p>
      )}
    </motion.div>
  )
}

// Shared layout animation — element "moves" between positions
// Wrap both with LayoutGroup and give them matching layoutId
import { LayoutGroup } from 'framer-motion'

function Tabs({ tabs }) {
  const [active, setActive] = useState(tabs[0].id)
  return (
    <LayoutGroup>
      <div className="flex gap-2">
        {tabs.map(tab => (
          <button key={tab.id} onClick={() => setActive(tab.id)} className="relative px-4 py-2">
            {tab.label}
            {active === tab.id && (
              <motion.div
                layoutId="active-indicator"   // Same ID = shared animation
                className="absolute inset-0 bg-indigo-500 rounded-md"
                style={{ zIndex: -1 }}
              />
            )}
          </button>
        ))}
      </div>
    </LayoutGroup>
  )
}

Gestures: Hover, Tap, Drag

// Hover and tap
<motion.button
  whileHover={{ scale: 1.05, boxShadow: '0 8px 32px rgba(99,102,241,0.3)' }}
  whileTap={{ scale: 0.97 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
  Click me
</motion.button>

// Draggable card with constraints
function DraggableCard() {
  const constraintsRef = useRef(null)
  return (
    <div ref={constraintsRef} className="relative h-64 w-full border rounded-xl overflow-hidden">
      <motion.div
        drag
        dragConstraints={constraintsRef}
        dragElastic={0.2}
        whileDrag={{ scale: 1.05, cursor: 'grabbing' }}
        className="absolute w-24 h-24 bg-indigo-500 rounded-xl cursor-grab flex items-center justify-center text-white"
      >
        Drag me
      </motion.div>
    </div>
  )
}

// Drag to reorder — combined with useDragControls
import { useDragControls } from 'framer-motion'

function SortableItem({ item }) {
  const controls = useDragControls()
  return (
    <motion.div drag="y" dragControls={controls} dragListener={false}>
      <span onPointerDown={(e) => controls.start(e)}>⠿</span>
      {item.title}
    </motion.div>
  )
}

Scroll-Driven Animations

import { motion, useScroll, useTransform, useInView } from 'framer-motion'

// Parallax effect
function ParallaxHero() {
  const { scrollY } = useScroll()
  const y = useTransform(scrollY, [0, 500], [0, -150])   // Map scroll to y offset

  return (
    <div className="relative h-screen overflow-hidden">
      <motion.img src="/hero.jpg" alt="Hero" style={{ y }} className="absolute inset-0 w-full h-[120%] object-cover" />
    </div>
  )
}

// Animate when element scrolls into view
function AnimateOnScroll({ children }) {
  const ref = useRef(null)
  const isInView = useInView(ref, { once: true, margin: '-100px' })

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 40 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 40 }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  )
}

// Progress bar tied to scroll position
function ReadingProgress() {
  const { scrollYProgress } = useScroll()
  return (
    <motion.div
      className="fixed top-0 left-0 right-0 h-1 bg-indigo-500 origin-left z-50"
      style={{ scaleX: scrollYProgress }}
    />
  )
}

Spring Physics

// Spring types
transition={{ type: 'spring', stiffness: 300, damping: 30 }}   // Bouncy
transition={{ type: 'spring', stiffness: 100, damping: 20 }}   // Slow bounce
transition={{ type: 'spring', stiffness: 400, damping: 50 }}   // Snappy, no bounce

// useSpring for value-driven animations
import { useSpring, motion } from 'framer-motion'

function SpringCounter({ value }) {
  const spring = useSpring(value, { stiffness: 100, damping: 30 })

  return <motion.span>{spring}</motion.span>
}

Performance Best Practices

  • Animate only transform and opacity — these run on the GPU compositor thread without triggering layout/paint. Avoid animating width, height, top, left
  • Use will-change: transform on elements that animate frequently — but sparingly (each creates a compositor layer)
  • Prefer layout prop over animating dimensions — Framer Motion uses FLIP technique which is GPU-accelerated
  • Set once: true on useInView — stops observing after first trigger, avoids scroll-event overhead
  • Use LazyMotion to tree-shake unused features in production bundles
import { LazyMotion, domAnimation, m } from 'framer-motion'

// Load only DOM animations (not 3D) — smaller bundle
function App() {
  return (
    <LazyMotion features={domAnimation}>
      <m.div animate={{ opacity: 1 }}>Uses smaller bundle</m.div>
    </LazyMotion>
  )
}