Skip to content
Animation

Framer Motion — Component Library Guide

Production-ready motion library for React. Declarative animations, gestures, layout animations, and exit transitions with a spring-first physics engine.

★ Editor's Pick TypeScript MIT
44.6M weekly downloads
33.4k GitHub stars
64.0 kB min+gzip bundle size
v13.1.1 latest version

Figures above measured on from the npm registry (latest version, last-week downloads) and the GitHub API (stars). Not copied from the project's own README, and re-measured rather than edited by hand, so the date tells you exactly how old these numbers are.

How the bundle size was measured. We installed framer-motion@13.1.1 on its own, bundled export * from 'framer-motion' with esbuild (esm, browser, minified, production), marked React external because your app already has it, and gzipped the result. Measured . This is the ceiling, not the typical cost: it imports the entire public surface with no tree-shaking credit. Pull one component from a well-shaken library and you'll pay a fraction of it. We publish the ceiling because it's the number we can reproduce, and reproducing it is the point.

animationgesturesspringlayoutexit-animations

TL;DR: Framer Motion is the go-to animation library for React. Wrap any element in <motion.div>, pass animate props, and get spring-physics-driven transitions. AnimatePresence handles exit animations that plain CSS can't. At ~97kB it's not tiny, but it's worth it. Official docs: framer.com/motion.

What is Framer Motion?

Framer Motion is a declarative animation library for React that covers the full spectrum from micro-interactions to complex orchestrated sequences. Its spring physics engine produces animations that feel natural rather than mechanical, and its AnimatePresence component solves one of React's hardest problems: animating elements as they leave the DOM.

I used to fake exit animations with a setTimeout and a visible boolean, delaying the actual unmount just long enough for a CSS transition to play. It worked, mostly, until a fast click sequence left ghost elements stuck mid-fade. Swapping that whole pattern for AnimatePresence took about twenty minutes and deleted three separate timeout hacks scattered across the codebase.

When to use it

Use Framer Motion for:

  • Exit animations - CSS can't animate elements being removed from the DOM; Framer can
  • Layout animations - automatically animate position/size changes when items reorder
  • Gesture-driven interactions - drag, swipe, hover with spring physics
  • Complex orchestration - stagger, sequence, and synchronize multiple animations

For simple hover/focus effects, CSS transitions are faster and have zero runtime cost. Framer Motion's ~97kB (gzipped ~30kB) is worth it only when you need what CSS can't do.

Key Features

  • motion.* components - drop-in replacements for any HTML/SVG element
  • AnimatePresence - enables exit animations for unmounted components
  • The layout prop - animates position/size changes automatically
  • Variants - named animation states for orchestrating complex sequences
  • Gestures - whileHover, whileTap, whileDrag with spring physics
  • Scroll animations - useScroll, useTransform for scroll-linked effects

Installation

npm install framer-motion

Basic Usage

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

// Simple enter animation
function FadeIn({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  )
}

// Exit animation with AnimatePresence
function Toast({ show, message }: { show: boolean; message: string }) {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          initial={{ opacity: 0, y: -20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
        >
          {message}
        </motion.div>
      )}
    </AnimatePresence>
  )
}

Stagger Children with Variants

const container = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: { staggerChildren: 0.1 },
  },
}

const item = {
  hidden: { opacity: 0, x: -20 },
  show: { opacity: 1, x: 0 },
}

function AnimatedList({ items }: { items: string[] }) {
  return (
    <motion.ul variants={container} initial="hidden" animate="show">
      {items.map((text) => (
        <motion.li key={text} variants={item}>
          {text}
        </motion.li>
      ))}
    </motion.ul>
  )
}

Scroll-linked Animations

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

function ParallaxHero() {
  const { scrollY } = useScroll()
  const y = useTransform(scrollY, [0, 500], [0, -150])

  return (
    <motion.div style={{ y }}>
      <img src="/hero.jpg" alt="Hero" />
    </motion.div>
  )
}

Draggable Card with Gesture Constraints

drag plus dragConstraints is the pattern I reach for on swipe-to-dismiss cards and reorderable lists where a full drag-and-drop library would be overkill:

import { motion, useMotionValue, useTransform } from 'framer-motion'

function SwipeToDismissCard({ onDismiss }: { onDismiss: () => void }) {
  const x = useMotionValue(0)
  const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])

  return (
    <motion.div
      style={{ x, opacity }}
      drag="x"
      dragConstraints={{ left: 0, right: 0 }}
      dragElastic={0.7}
      onDragEnd={(_, info) => {
        if (Math.abs(info.offset.x) > 150) onDismiss()
      }}
    >
      Swipe me away
    </motion.div>
  )
}

dragElastic controls how far the card can travel past the constraint before snapping back, and onDragEnd reads the final offset to decide whether the swipe counted as a dismissal or just a nudge. No separate touch-event listeners, no manual velocity math.

TypeScript Tips

Use Variants type for typed animation states:

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

const cardVariants: Variants = {
  offscreen: { y: 50, opacity: 0 },
  onscreen: {
    y: 0,
    opacity: 1,
    transition: { type: 'spring', bounce: 0.4, duration: 0.8 },
  },
}

Common Gotchas

  • AnimatePresence needs a key - the child must have a stable key prop for exit animations to trigger correctly
  • Layout animations and overflow: hidden - parent elements with overflow: hidden clip layout animations; add layout to the parent too or use layoutId
  • Bundle size - if you only need scroll animations, import from framer-motion/client or use the new Motion One alternative
  • initial={false} - pass to AnimatePresence to skip enter animations on first render (important for SSR)

What It Costs You

Framer Motion is the most capable animation library in the React ecosystem and also one of the heavier dependencies you'll add for something users can turn off. Worth weighing before it goes in.

The bundle is the obvious cost. A fade-in on scroll and a hover scale do not need a full animation runtime; the CSS equivalents ship for nothing and run off the main thread. Reach for this when you need orchestration that CSS genuinely can't express: shared layout transitions between routes, gesture-driven drags with momentum, exit animations that must finish before an element unmounts. Those are real gaps, and nothing else fills them as well.

The cost people forget is accessibility. Layout animations move content while someone is reading it, and for a reader with a vestibular disorder that isn't a flourish, it's a symptom. Wrap the app in MotionConfig with reducedMotion="user" so every animation respects the OS setting by default, rather than remembering a check at each call site. You'll forget one otherwise, and the one you forget will be the page transition.

Last, animate transform and opacity and leave the rest alone. Animating width, height, or top forces layout on every frame, and no library makes that cheap. The layout prop exists precisely so you can get the visual result of a size change through a transform instead.

  • React 19 Guide - useOptimistic produces pending states you can animate smoothly with Framer Motion's layout animations
  • Vaul - drawer component with built-in gesture animations; Framer Motion for more custom animation control
  • dnd kit - sortable drag-and-drop; combine with Framer Motion layout prop for animated reordering

Frequently Asked Questions

Is Framer Motion too large for production use?

At roughly 30kB gzipped, Framer Motion is heavier than CSS transitions. It is worth the cost when you need features CSS cannot provide: exit animations for unmounting elements, layout animations, and gesture-driven interactions. For simple hover and focus effects, stick with CSS.

How do I animate an element when it is removed from the DOM?

Wrap the element in AnimatePresence and give it an exit prop with the animation values. AnimatePresence keeps the element mounted long enough to complete the exit animation before removing it from the DOM. The child must have a key prop for this to work.

What is the difference between initial, animate, and exit props?

initial defines the state before the element enters. animate defines the target state it animates to on mount. exit defines the state it animates to before unmounting. These three props cover the full enter-idle-leave lifecycle.

Can I use Framer Motion with React Server Components?

Framer Motion components are client-side only. Mark any file that uses motion components with 'use client'. Static layouts and server components can wrap client components that use Framer Motion without any issues.

What are Framer Motion variants?

Variants are named animation states defined as an object. You pass variant names as strings to initial and animate props instead of inline values. This enables parent-to-child animation orchestration and staggering children without prop drilling.