Framer Motion: The Complete Guide for React & Next.js Developers

22 min read
Abstract motion blur representing smooth animations in web development

What is Framer Motion? Framer Motion is a declarative animation library for React that replaces CSS keyframes with JavaScript objects. It provides the motion component wrapper, AnimatePresence for exit animations, and automatic layout animations. Complex sequences that would take 100 lines of CSS become 10 lines of code.

I’ve shipped Framer Motion in production apps handling millions of users. This guide distills those lessons: what actually works, what breaks under pressure, and the patterns worth adopting. Skip the theory. This is the practical playbook.

TL;DR: Replace <div> with <motion.div>, define animations with initial/animate/exit props, wrap conditionally rendered elements in AnimatePresence with a unique key, and stick to transform/opacity for 60fps performance. Use LazyMotion with domAnimation to cut bundle size from 30kb to 15kb.

Last tested with Framer Motion 12.25.0 and Next.js 16.1.1 in January 2026


Getting Started

Replace <div> with <motion.div>, add animation props, done. That’s the core mental model.

This guide covers Framer Motion v10+ (also known as “Motion”). Most patterns work in v6+, but useAnimate, useInView, and some LazyMotion features require v10. Check your version with npm list framer-motion.

Installation

npm install framer-motion
# or
yarn add framer-motion
# or
pnpm add framer-motion

For Next.js App Router projects, remember that motion components are client-side only. Add the "use client" directive at the top of any file using Framer Motion:

"use client";

import { motion } from "framer-motion";

The Mental Model

Replace standard HTML elements with their motion equivalents. A <div> becomes <motion.div>, a <button> becomes <motion.button>. These components accept animation props:

import { motion } from "framer-motion";

function FadeInBox() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.5 }}
      className="p-6 bg-blue-500 rounded-lg"
    >
      I fade in on mount
    </motion.div>
  );
}

The three core props are:

  • initial: The starting state (can be false to disable mount animations)
  • animate: The target state to animate toward
  • transition: How the animation should behave (duration, easing, spring physics)

Copy/Paste Starter Kit

Basic Motion Component:

import { motion } from "framer-motion";

export function SlideIn({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4, ease: "easeOut" }}
    >
      {children}
    </motion.div>
  );
}

Variants Example:

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};

export function StaggeredList({ items }: { items: string[] }) {
  return (
    <motion.ul variants={containerVariants} initial="hidden" animate="visible">
      {items.map((item) => (
        <motion.li key={item} variants={itemVariants}>
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}

AnimatePresence Example:

import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";

export function ToggleContent() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <>
      <button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
      <AnimatePresence>
        {isVisible && (
          <motion.div
            key="content"
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: "auto" }}
            exit={{ opacity: 0, height: 0 }}
          >
            Content that animates in and out
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
}

Reduced Motion Example:

import { motion, useReducedMotion } from "framer-motion";

export function AccessibleSlideIn({ children }: { children: React.ReactNode }) {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={{ opacity: 0, x: shouldReduceMotion ? 0 : -100 }}
      animate={{ opacity: 1, x: 0 }}
      transition={{ duration: shouldReduceMotion ? 0.1 : 0.4 }}
    >
      {children}
    </motion.div>
  );
}

Do/Don’t List

DoDon’t
Use motion.div for animatable elementsExpect animate/initial props to work on regular div
Add "use client" in Next.js App RouterForget the directive and get cryptic errors
Start with simple opacity/transform animationsJump straight to complex layout animations
Test animations on lower-powered devicesOnly test on your development machine

Field Notes: Getting Started

  • What works: Simple fade and slide animations are nearly bulletproof. Start here before adding complexity.
  • What breaks: Forgetting "use client" in Next.js causes a createContext error that doesn’t immediately point to Framer Motion.
  • How to fix it: If you see context-related errors, check that all files importing from framer-motion have the client directive.
  • Practical recommendation: Create a components/motion folder with pre-configured motion components that include your app’s default transitions.

Framer Motion vs Alternatives

How does Framer Motion compare to other animation libraries? Framer Motion offers the best React integration and automatic layout animations, but comes with a larger bundle than CSS-only solutions. Here’s how it stacks up:

FeatureFramer MotionCSS AnimationsReact SpringGSAP
Bundle Size~30kb (full) / ~15kb (lazy)0kb~25kb~60kb
Exit Animations✅ AnimatePresence❌ Manual JS required⚠️ useTransition (complex)✅ Manual timeline
Layout Animations✅ Automatic FLIP❌ Not supported❌ Not supported⚠️ Manual FLIP
Gesture Support✅ Built-in drag, hover, tap❌ JS required❌ Separate library✅ Draggable plugin
Learning CurveMediumLowHigh (physics model)High (timeline API)
React IntegrationNative (declarative)CSS-in-JS neededNativeWrapper/refs needed
SSR Support✅ With hydration handling✅ Native✅ Native⚠️ Complex setup
TypeScript✅ First-classN/A✅ First-class⚠️ @types package

When to choose Framer Motion: You need exit animations, layout animations, or gesture support without writing imperative code. The declarative API integrates naturally with React’s component model.

When to choose alternatives:

  • CSS Animations: Simple hover states, loading spinners. No library needed
  • React Spring: Physics-based animations where you need precise spring control
  • GSAP: Complex timelines, SVG morphing, or ScrollTrigger-heavy pages

When to Use Each Animation Approach

Framer Motion offers multiple ways to animate. Here’s when to use each:

Use Inline animate Props When:

  • The animation is simple (fade, slide, scale)
  • It’s a one-off animation not reused elsewhere
  • You’re animating based on a single boolean state
// Simple fade-in on mount
<motion.div animate={{ opacity: 1 }} initial={{ opacity: 0 }} />

Use Variants When:

  • Parent and child components need coordinated animations
  • You want staggered list animations with staggerChildren
  • The same animation states are reused across components
  • You need named states like “hidden”, “visible”, “exit”
// Coordinated parent-child animations
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
  <motion.li variants={itemVariants} />
</motion.ul>

Use useAnimate When:

  • Animations depend on async operations (API calls, clipboard copy)
  • You need precise sequencing control beyond variants
  • The animation is imperative. It fires once and doesn’t persist in state
// Sequence after async operation
const [scope, animate] = useAnimate();
await copyToClipboard();
await animate(scope.current, { scale: 1.1 });

Use Layout Animations When:

  • Element size or position changes based on content/state
  • You’re building reorderable lists or grids
  • You need “shared element” transitions between views with layoutId
// Automatic size/position animation
<motion.div layout style={{ width: isExpanded ? 400 : 200 }} />

Use useMotionValue + useTransform When:

  • Animations should respond to continuous input (scroll, drag position)
  • You need to derive one animated value from another
  • Performance is critical. This approach avoids React re-renders
// Scroll-linked parallax
const { scrollY } = useScroll();
const opacity = useTransform(scrollY, [0, 300], [1, 0]);

Core Concepts (variants, gestures)

What are Framer Motion variants? Variants are named animation states that coordinate animations across parent and child components. Instead of passing animation objects directly, you reference variant names as strings, enabling orchestrated sequences with staggerChildren and delayChildren.

Variants: Orchestrated Animations

Variants let you define named animation states and coordinate animations across parent and child components. Instead of passing animation objects directly, you reference variant names as strings.

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,
    },
  },
};

export function AnimatedCard({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      variants={cardVariants}
      initial="offscreen"
      whileInView="onscreen"
      viewport={{ once: true, amount: 0.3 }}
      className="bg-white rounded-xl shadow-lg p-6"
    >
      {children}
    </motion.div>
  );
}

The Variants type from Framer Motion provides full TypeScript support for variant objects. For more on React patterns, see the React Hooks guide.

Parent-Child Orchestration

Variants flow down through nested motion components. Children automatically inherit variant names from parents, enabling staggered animations without explicit coordination:

const containerVariants: Variants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      delayChildren: 0.3,
      staggerChildren: 0.1,
      // Pro tip: use staggerDirection: -1 to reverse the stagger order
    },
  },
};

const itemVariants: Variants = {
  hidden: { y: 20, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { type: "spring", stiffness: 100 },
  },
};

export function NavigationMenu({ items }: { items: NavItem[] }) {
  return (
    <motion.nav
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item) => (
        <motion.a
          key={item.href}
          href={item.href}
          variants={itemVariants}
          className="block py-2 px-4 hover:bg-gray-100"
        >
          {item.label}
        </motion.a>
      ))}
    </motion.nav>
  );
}

Notice that child motion.a elements don’t need initial or animate props. They inherit the variant names from the parent and use their own itemVariants to define what those states mean.

Gestures: Interactive Animations

Framer Motion provides gesture props that make interactive animations trivial:

export function InteractiveButton({ children }: { children: React.ReactNode }) {
  return (
    <motion.button
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      transition={{ type: "spring", stiffness: 400, damping: 17 }}
      className="px-6 py-3 bg-indigo-600 text-white rounded-lg"
    >
      {children}
    </motion.button>
  );
}

Drag Gestures

The drag prop enables draggable elements with physics-based behavior:

export function DraggableCard() {
  return (
    <motion.div
      drag
      dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
      dragElastic={0.2}
      whileDrag={{ scale: 1.1, cursor: "grabbing" }}
      className="w-32 h-32 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl cursor-grab"
    />
  );
}

For constrained dragging within a parent container:

import { motion } from "framer-motion";
import { useRef } from "react";

export function DraggableWithinBounds() {
  const constraintsRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={constraintsRef} className="w-full h-64 bg-gray-100 rounded-lg">
      <motion.div
        drag
        dragConstraints={constraintsRef}
        dragMomentum={false}
        className="w-16 h-16 bg-blue-500 rounded-lg"
      />
    </div>
  );
}

Pointer-Friendly UX Patterns

When building touch-friendly interfaces, combine gestures thoughtfully:

export function SwipeableCard({ onDismiss }: { onDismiss: () => void }) {
  return (
    <motion.div
      drag="x"
      dragConstraints={{ left: 0, right: 0 }}
      onDragEnd={(_, info) => {
        if (Math.abs(info.offset.x) > 100) {
          onDismiss();
        }
      }}
      whileTap={{ cursor: "grabbing" }}
      className="bg-white rounded-xl shadow-lg p-4"
    >
      Swipe to dismiss
    </motion.div>
  );
}

Do/Don’t List

DoDon’t
Use variants for coordinated parent-child animationsDefine animations inline when they’re reused
Keep staggerChildren values small (0.05-0.15s)Use large stagger values that feel sluggish
Add dragConstraints to prevent elements flying off-screenEnable drag without boundaries
Use whileTap for touch feedback on mobileRely only on whileHover which doesn’t work on touch

Field Notes: Variants and Gestures

  • What works: Variants with staggerChildren create professional-feeling list animations with minimal code. The parent-child inheritance is intuitive once you understand it.
  • What breaks: Adding an animate prop to a child component breaks variant inheritance. The child will ignore the parent’s variant state.
  • How to fix it: Remove explicit animate props from children that should inherit from parents. Only define variants on children.
  • Practical recommendation: Create a variants library file (lib/motion-variants.ts) with your app’s standard animation patterns. This ensures consistency and makes animations easy to audit. If you’re building a component library, see the design systems guide for organizing reusable motion patterns.

Scroll Animations

How do I create scroll-linked animations in Framer Motion? Use the useScroll hook to get MotionValue objects representing scroll progress (0 to 1), then useTransform to map those values to animation properties. This approach uses requestAnimationFrame under the hood and avoids React re-renders for smooth 60fps performance.

The useScroll Hook

The useScroll hook returns MotionValue objects representing scroll progress. Under the hood, it uses Intersection Observer for element tracking and requestAnimationFrame for smooth updates without triggering React re-renders:

import { motion, useScroll } from "framer-motion";

export function ScrollProgressBar() {
  // Track page scroll progress (0 to 1)
  const { scrollYProgress } = useScroll();

  return (
    <motion.div
      style={{ scaleX: scrollYProgress }}
      className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left z-50"
    />
  );
}

For tracking scroll within a specific container:

import { motion, useScroll } from "framer-motion";
import { useRef } from "react";

export function ContainerScrollProgress() {
  const containerRef = useRef<HTMLDivElement>(null);
  
  const { scrollYProgress } = useScroll({
    container: containerRef,
  });

  return (
    <div ref={containerRef} className="h-96 overflow-y-auto">
      <motion.div style={{ opacity: scrollYProgress }}>
        Content that fades in as you scroll
      </motion.div>
    </div>
  );
}

Mapping Scroll to Animations with useTransform

The useTransform hook maps one range of values to another. Perfect for creating parallax effects:

import { motion, useScroll, useTransform } from "framer-motion";

export function ParallaxHero() {
  const { scrollY } = useScroll();
  
  // Map scroll position to y offset (slower than scroll = parallax)
  const y = useTransform(scrollY, [0, 500], [0, 150]);
  const opacity = useTransform(scrollY, [0, 300], [1, 0]);

  return (
    <motion.div
      style={{ y, opacity }}
      className="h-screen flex items-center justify-center bg-gradient-to-b from-purple-600 to-blue-600"
    >
      <h1 className="text-6xl font-bold text-white">
        Parallax Hero
      </h1>
    </motion.div>
  );
}

Scroll-Triggered Element Animations

Track when a specific element enters the viewport using the target option:

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

export function RevealOnScroll({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"], // When element enters/leaves viewport
  });

  const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]);
  const scale = useTransform(scrollYProgress, [0, 0.3], [0.8, 1]);

  return (
    <motion.div ref={ref} style={{ opacity, scale }}>
      {children}
    </motion.div>
  );
}

For simpler visibility detection without scroll-linked values, use the useInView hook:

import { motion, useInView } from "framer-motion";
import { useRef } from "react";

export function FadeInWhenVisible({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  const isInView = useInView(ref, { once: true, margin: "-100px" });

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

Do/Don’t List

DoDon’t
Use useTransform for smooth value mappingManually calculate positions in useEffect
Apply will-change: transform sparinglyAdd will-change to many elements
Clean up scroll listeners (hooks do this automatically)Create manual scroll listeners without cleanup
Combine with whileInView for simpler reveal effectsOvercomplicate with useScroll when whileInView suffices

Field Notes: Scroll Animations

  • What works: Parallax and progress indicators are nearly plug-and-play with useScroll and useTransform. The hooks handle cleanup automatically.
  • What breaks: Performance can degrade if you animate expensive properties (like blur) based on scroll. Mobile browsers may throttle scroll events.
  • How to fix it: Stick to transform and opacity. Use useSpring to smooth out jerky scroll values if needed.
  • Practical recommendation: For simple “fade/slide in when visible” effects, prefer whileInView or useInView over scroll hooks. They’re simpler and more performant.

Layout Animations

How do Framer Motion layout animations work? The layout prop uses the FLIP technique (First, Last, Invert, Play) to animate layout changes using GPU-accelerated transform instead of expensive properties like width and height. The browser measures positions before and after state changes, then Framer Motion applies an inverse transform and animates it back to zero. This keeps animations on the compositor thread.

FLIP technique: First, Last, Invert, Play animation flow

import { motion } from "framer-motion";
import { useState } from "react";

export function ExpandableCard() {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <motion.div
      layout
      onClick={() => setIsExpanded(!isExpanded)}
      className={`bg-white rounded-xl shadow-lg cursor-pointer ${
        isExpanded ? "p-8" : "p-4"
      }`}
      style={{ width: isExpanded ? 400 : 200 }}
    >
      <motion.h3 layout="position" className="font-bold">
        {/* layout="position" prevents text from scaling/squishing during the box resize */}
        Click to expand
      </motion.h3>
      {isExpanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          className="mt-4 text-gray-600"
        >
          Additional content appears here with a smooth animation.
        </motion.p>
      )}
    </motion.div>
  );
}

List Reordering

Layout animations enable smooth list reordering with the Reorder component:

import { motion, Reorder } from "framer-motion";
import { useState } from "react";

interface Task {
  id: string;
  title: string;
}

export function ReorderableTaskList() {
  const [tasks, setTasks] = useState<Task[]>([
    { id: "1", title: "Review pull requests" },
    { id: "2", title: "Update documentation" },
    { id: "3", title: "Fix accessibility issues" },
  ]);

  return (
    <Reorder.Group
      axis="y"
      values={tasks}
      onReorder={setTasks}
      className="space-y-2"
    >
      {tasks.map((task) => (
        <Reorder.Item
          key={task.id}
          value={task}
          className="bg-white p-4 rounded-lg shadow cursor-grab active:cursor-grabbing"
        >
          {task.title}
        </Reorder.Item>
      ))}
    </Reorder.Group>
  );
}

Shared Layout Animations with layoutId

The layoutId prop creates “magic motion” effects where two separate elements animate between each other:

import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";

interface Item {
  id: string;
  title: string;
  description: string;
}

export function ExpandableGrid({ items }: { items: Item[] }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const selectedItem = items.find((item) => item.id === selectedId);

  return (
    <>
      <div className="grid grid-cols-3 gap-4">
        {items.map((item) => (
          <motion.div
            key={item.id}
            layoutId={item.id}
            onClick={() => setSelectedId(item.id)}
            className="bg-white p-4 rounded-xl shadow-lg cursor-pointer"
          >
            <motion.h3 layoutId={`title-${item.id}`}>{item.title}</motion.h3>
          </motion.div>
        ))}
      </div>

      <AnimatePresence>
        {selectedId && selectedItem && (
          <motion.div
            layoutId={selectedId}
            className="fixed inset-0 z-50 flex items-center justify-center"
            onClick={() => setSelectedId(null)}
          >
            <motion.div className="bg-white p-8 rounded-xl shadow-2xl max-w-lg">
              <motion.h3 layoutId={`title-${selectedId}`} className="text-2xl font-bold">
                {selectedItem.title}
              </motion.h3>
              <motion.p
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                className="mt-4"
              >
                {selectedItem.description}
              </motion.p>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
}

Layout Animation Options

The layout prop accepts different values for specific behaviors:

  • layout={true} - Animate both position and size
  • layout="position" - Only animate position (useful for images/text that shouldn’t scale)
  • layout="size" - Only animate size changes
// For images that shouldn't distort during layout changes
<motion.img
  layout="position"
  src="/avatar.jpg"
  className="w-16 h-16 rounded-full"
/>

Advanced: Scrollable Containers and Fixed Elements

When animating within scrollable containers, add layoutScroll to account for scroll offset:

<motion.div layoutScroll className="overflow-y-auto h-96">
  {items.map((item) => (
    <motion.div key={item.id} layout>
      {item.content}
    </motion.div>
  ))}
</motion.div>

For fixed-position containers, use layoutRoot:

<motion.div layoutRoot className="fixed top-0 left-0">
  <motion.div layout>
    Content that animates correctly within fixed container
  </motion.div>
</motion.div>

Anti-Pattern: Accidental Layout Triggers

A common mistake is animating CSS properties that trigger browser layout recalculations, defeating the performance benefits:

// ❌ Bad: Animating width triggers layout every frame
<motion.div
  animate={{ width: isExpanded ? 400 : 200 }}
  className="bg-white rounded-lg"
/>

// ✅ Good: Use layout prop for automatic transform-based animation
<motion.div
  layout
  style={{ width: isExpanded ? 400 : 200 }}
  className="bg-white rounded-lg"
/>

Do/Don’t List

DoDon’t
Use layout prop for size/position changesAnimate width/height directly
Add layout="position" for images and textLet images distort during layout animations
Use layoutScroll in scrollable containersForget scroll offset causes misaligned animations
Set border-radius via style prop for scale correctionUse Tailwind classes for border-radius with layout

Field Notes: Layout Animations

  • What works: The layout prop handles 90% of layout animation needs automatically. List reordering with Reorder.Group feels magical with minimal code.
  • What breaks: Elements with display: inline won’t animate. Browsers don’t apply transforms to inline elements. Border and box-shadow can look stretched during scale animations.
  • How to fix it: Ensure elements are block or inline-block. For borders, use a wrapper element with padding instead. Set border-radius and box-shadow via the style prop for automatic scale correction.
  • Practical recommendation: Start with layout={true}, then refine to layout="position" for elements that shouldn’t scale. Use LayoutGroup when multiple unrelated components affect each other’s layout.

Exit Animations

Why don’t my Framer Motion exit animations work? Exit animations require three things: (1) AnimatePresence wraps the conditional (not inside it), (2) the motion component has a unique key prop, and (3) the motion component is a direct child of AnimatePresence. Missing any causes silent failures.

// ❌ Wrong: AnimatePresence inside the conditional
{isVisible && (
  <AnimatePresence>
    <motion.div exit={{ opacity: 0 }}>Content</motion.div>
  </AnimatePresence>
)}

// ✅ Correct: AnimatePresence wraps the conditional
<AnimatePresence>
  {isVisible && (
    <motion.div key="content" exit={{ opacity: 0 }}>Content</motion.div>
  )}
</AnimatePresence>

Basic Exit Animation Pattern

import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";

export function NotificationToast() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <AnimatePresence>
      {isVisible && (
        <motion.div
          key="toast"
          initial={{ opacity: 0, y: 50, scale: 0.9 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: 20, scale: 0.9 }}
          transition={{ type: "spring", damping: 25, stiffness: 300 }}
          className="fixed bottom-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg"
        >
          <p>Changes saved successfully!</p>
          <button
            onClick={() => setIsVisible(false)}
            className="ml-4 text-green-100 hover:text-white"
          >
            Dismiss
          </button>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

A complete modal implementation with coordinated exit animations:

import { motion, AnimatePresence } from "framer-motion";

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

const backdropVariants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1 },
};

const modalVariants = {
  hidden: { opacity: 0, scale: 0.95, y: 20 },
  visible: { 
    opacity: 1, 
    scale: 1, 
    y: 0,
    transition: { type: "spring", damping: 25, stiffness: 300 }
  },
  exit: { 
    opacity: 0, 
    scale: 0.95, 
    y: 20,
    transition: { duration: 0.2 }
  },
};

export function Modal({ isOpen, onClose, children }: ModalProps) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          <motion.div
            key="backdrop"
            variants={backdropVariants}
            initial="hidden"
            animate="visible"
            exit="hidden"
            onClick={onClose}
            className="fixed inset-0 bg-black/50 z-40"
          />
          <motion.div
            key="modal"
            variants={modalVariants}
            initial="hidden"
            animate="visible"
            exit="exit"
            className="fixed inset-0 z-50 flex items-center justify-center p-4"
          >
            <div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
              {children}
            </div>
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
}

Animated List Item Removal

For lists where items can be added or removed:

import { motion, AnimatePresence } from "framer-motion";

interface Notification {
  id: string;
  message: string;
}

export function NotificationStack({ 
  notifications, 
  onDismiss 
}: { 
  notifications: Notification[];
  onDismiss: (id: string) => void;
}) {
  return (
    <div className="fixed top-4 right-4 space-y-2 z-50">
      <AnimatePresence mode="popLayout">
        {notifications.map((notification) => (
          <motion.div
            key={notification.id}
            layout
            initial={{ opacity: 0, x: 100, scale: 0.9 }}
            animate={{ opacity: 1, x: 0, scale: 1 }}
            exit={{ opacity: 0, x: 100, scale: 0.9 }}
            transition={{ type: "spring", damping: 25, stiffness: 300 }}
            className="bg-white rounded-lg shadow-lg p-4 min-w-[300px]"
          >
            <p>{notification.message}</p>
            <button
              onClick={() => onDismiss(notification.id)}
              className="text-gray-400 hover:text-gray-600 mt-2"
            >
              Dismiss
            </button>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

The mode="popLayout" prop ensures remaining items animate smoothly when one is removed.

Route Transitions

For page transitions in Next.js or React Router, wrap your route content:

"use client";

// In your layout or app component
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";

export function PageTransitionLayout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();

  return (
    <AnimatePresence mode="wait">
      <motion.main
        key={pathname}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3 }}
      >
        {children}
      </motion.main>
    </AnimatePresence>
  );
}

Do/Don’t List

DoDon’t
Always provide a unique key to exiting elementsForget keys and wonder why exit doesn’t work
Place AnimatePresence outside conditionalsNest AnimatePresence inside the conditional
Use mode="wait" for sequential transitionsLet enter/exit animations overlap unintentionally
Keep exit animations short (150-300ms)Make users wait for long exit animations

Field Notes: Exit Animations

  • What works: Simple opacity and transform exit animations are reliable. The mode="popLayout" option handles list removals elegantly.
  • What breaks: Non-motion elements inside AnimatePresence can prevent exit animations. HOC-wrapped components sometimes lose the exit animation.
  • How to fix it: Ensure all direct children of AnimatePresence are motion components. For HOCs, use forwardRef and ensure the motion component receives the key prop.
  • Practical recommendation: Create a reusable FadePresence wrapper component that handles the AnimatePresence boilerplate for common use cases like modals and toasts.

Performance Tips

Understanding browser rendering helps you make informed animation decisions. Janky animations hurt UX more than no animations.

The Rendering Pipeline

According to web.dev’s animation guide, browsers render in three main steps:

  1. Layout: Calculate element geometry (triggered by width, height, margin, padding, top, left)
  2. Paint: Draw pixels for each layer (triggered by background-color, border-radius, box-shadow)
  3. Composite: Combine layers on the GPU (triggered by transform, opacity, filter)

The browser rendering pipeline: Layout → Paint → Composite

Triggering an earlier step forces all subsequent steps to run. Composite-only animations are the most performant because they skip layout and paint entirely. They run on a separate compositor thread that isn’t blocked by JavaScript on the main thread. This is why Framer Motion’s layout prop uses transform internally, even when animating size changes.

Prefer Transform and Opacity

The Motion performance tier list categorizes animations by performance impact. Transform and opacity animations can run on the GPU compositor thread, remaining smooth even when the main thread is busy:

// ✅ S-Tier: Compositor-only, hardware accelerated
<motion.div
  animate={{ 
    x: 100,        // transform: translateX
    y: 50,         // transform: translateY
    scale: 1.1,    // transform: scale
    rotate: 45,    // transform: rotate
    opacity: 0.5   // opacity
  }}
/>

// ❌ D-Tier: Triggers layout every frame
<motion.div
  animate={{ 
    width: 200,    // Triggers layout
    height: 100,   // Triggers layout
    marginLeft: 20 // Triggers layout
  }}
/>

Understanding will-change

The CSS will-change property hints to the browser that an element will animate, allowing it to optimize ahead of time by promoting the element to its own compositor layer. Framer Motion applies this automatically during animations, but you can add it manually for elements that animate frequently:

// Framer Motion handles this automatically, but for custom CSS:
<motion.div
  style={{ willChange: "transform" }}
  whileHover={{ scale: 1.05 }}
/>

Caution: Don’t add will-change to many elements. Each creates a new GPU layer, consuming memory. Only use it on elements actively animating, and let Framer Motion manage it when possible.

Code Splitting with LazyMotion

Framer Motion is powerful, but that power comes with bytes. The full library is about 30-50kb gzipped. In production apps, shipping everything to the main bundle is a common mistake that hurts load-time performance.

Use LazyMotion with the m component to drastically reduce initial bundle size:

import { LazyMotion, domAnimation, m } from "framer-motion";

// 1. Replace <motion.div> with <m.div>
// 2. Wrap your app (or feature) in <LazyMotion>

function App({ children }: { children: React.ReactNode }) {
  return (
    <LazyMotion features={domAnimation}>
      {children}
    </LazyMotion>
  );
}

// Inside any component:
// ⚠️ Important: <m.div> will NOT animate unless wrapped in <LazyMotion> somewhere up the tree!
function FadeIn({ children }: { children: React.ReactNode }) {
  return (
    <m.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
    >
      {children}
    </m.div>
  );
}

Feature Bundles:

  • domAnimation (about 15kb): Covers animations, transitions, and exit. Use this for most apps.
  • domMax (about 27kb): Adds layout animations, drag, and more. Only import if you need these features.

For even more control, dynamically import features:

import { LazyMotion, m } from "framer-motion";

function App({ children }: { children: React.ReactNode }) {
  return (
    <LazyMotion features={() => import("framer-motion").then((mod) => mod.domMax)}>
      {children}
    </LazyMotion>
  );
}

Imperative Animations with useAnimate

Sometimes declarative variants are too rigid. The useAnimate hook provides imperative control for complex sequences or one-off animations:

import { useAnimate } from "framer-motion";

export function CopyButton({ text }: { text: string }) {
  const [scope, animate] = useAnimate();

  const handleCopy = async () => {
    await navigator.clipboard.writeText(text);
    
    // Sequence: flash green, then reset
    await animate(scope.current, { backgroundColor: "#22c55e" }, { duration: 0.1 });
    await animate(scope.current, { backgroundColor: "#ffffff" }, { duration: 0.3 });
  };

  return (
    <button ref={scope} onClick={handleCopy} className="px-4 py-2 rounded border">
      Copy
    </button>
  );
}

Use useAnimate when:

  • You need to chain animations that depend on async operations
  • State management for animation states feels like overkill
  • The animation fires once and doesn’t need to stay in a state

Reduce Re-renders

Motion components can trigger unnecessary re-renders. Extract motion components and memoize when appropriate:

import { motion } from "framer-motion";
import { memo } from "react";

// Extract animation variants outside component
const fadeInVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};

// Memoize motion components that receive stable props
const AnimatedCard = memo(function AnimatedCard({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <motion.div
      variants={fadeInVariants}
      initial="hidden"
      animate="visible"
      className="bg-white rounded-lg p-4 shadow"
    >
      {children}
    </motion.div>
  );
});

Spring Configuration

Springs create natural-feeling animations, but extreme values hurt performance:

Spring physics: stiffness vs. damping oscillation curves

// ✅ Good: Reasonable spring values
<motion.div
  animate={{ x: 100 }}
  transition={{ 
    type: "spring",
    stiffness: 300,  // 100-500 is typical
    damping: 25      // 10-40 is typical
  }}
/>

// ❌ Problematic: Very high stiffness causes many frames
<motion.div
  animate={{ x: 100 }}
  transition={{ 
    type: "spring",
    stiffness: 10000,  // Too high
    damping: 5         // Too low (bouncy)
  }}
/>

Transform Origin

When animating scale or rotate, the transform-origin determines the pivot point. By default, transforms originate from the element’s center. Override with the style prop:

// Scale from top-left corner
<motion.div
  style={{ transformOrigin: "top left" }}
  whileHover={{ scale: 1.1 }}
/>

// Rotate from bottom center (like a pendulum)
<motion.div
  style={{ transformOrigin: "bottom center" }}
  animate={{ rotate: [0, 10, -10, 0] }}
  transition={{ repeat: Infinity, duration: 2 }}
/>

Programmatic Control with useAnimationControls

For animations triggered by external events (not React state), use useAnimationControls:

import { motion, useAnimationControls } from "framer-motion";
import { useEffect } from "react";

export function ShakeOnError({ error }: { error: string | null }) {
  const controls = useAnimationControls();

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

  return (
    <motion.div animate={controls} className="p-4 border rounded">
      <input type="text" />
      {error && <p className="text-red-500">{error}</p>}
    </motion.div>
  );
}

React 18 Concurrent Mode Compatibility

Framer Motion v10+ is fully compatible with React 18’s Concurrent Mode and Strict Mode. Animations work correctly with <Suspense> boundaries and streaming SSR. If you’re using React 17, stick with Framer Motion v6-v9 for best compatibility.

Avoid Expensive Effects

Some CSS properties are particularly costly to animate:

// ❌ Expensive: Large blur radius
<motion.div
  animate={{ filter: "blur(50px)" }}  // Very expensive
/>

// ❌ Expensive: Box shadow changes
<motion.div
  animate={{ 
    boxShadow: "0 25px 50px rgba(0,0,0,0.5)"  // Triggers paint
  }}
/>

// ✅ Better: Animate a pseudo-element's opacity instead
// Pre-render the shadow, then fade it in/out

Accessibility: Respecting Reduced Motion

Users can enable “Reduce Motion” in their operating system settings to minimize animations that may cause discomfort. Framer Motion provides two approaches to respect this preference.

Global Configuration with MotionConfig:

import { MotionConfig } from "framer-motion";

export function App({ children }: { children: React.ReactNode }) {
  return (
    <MotionConfig reducedMotion="user">
      {children}
    </MotionConfig>
  );
}

Setting reducedMotion="user" automatically disables transform and layout animations while preserving opacity and color transitions. This is the recommended approach from the official docs. For a deeper dive into building accessible interfaces, see building interactive UI components.

Custom Control with useReducedMotion:

For more granular control, use the useReducedMotion hook:

import { motion, useReducedMotion } from "framer-motion";

export function Sidebar({ isOpen }: { isOpen: boolean }) {
  const shouldReduceMotion = useReducedMotion();
  
  // Replace slide animation with fade for reduced motion users
  const closedX = shouldReduceMotion ? 0 : "-100%";

  return (
    <motion.aside
      animate={{
        opacity: isOpen ? 1 : 0,
        x: isOpen ? 0 : closedX,
      }}
      transition={{ 
        duration: shouldReduceMotion ? 0.1 : 0.3 
      }}
      className="fixed left-0 top-0 h-full w-64 bg-gray-900"
    >
      {/* Sidebar content */}
    </motion.aside>
  );
}

Motion Accessibility Checklist:

  • Wrap app in <MotionConfig reducedMotion="user"> for automatic handling
  • Use useReducedMotion() for custom reduced-motion alternatives
  • Replace x/y transforms with opacity fades for reduced motion
  • Disable parallax and auto-playing animations
  • Keep essential transitions (like focus indicators) even with reduced motion
  • Test with “Reduce Motion” enabled in your OS settings

What Teams Run Into

Based on common issues developers encounter in production:

  1. SSR Hydration Mismatches: In Next.js, the data-projection-id prop can differ between server and client renders, causing console warnings. This is cosmetic but annoying. Suppress with suppressHydrationWarning on the motion component or ensure animations only run client-side. For a deeper understanding of server vs client components, see React Server Components guide.

  2. Initial Animation Flash: Components may flash their initial state before animating. Use initial={false} to skip mount animations, or ensure the initial state matches the server-rendered state.

  3. Exit Animations Not Playing: The most common issue. Usually caused by missing keys, AnimatePresence placement, or non-motion children. Always verify the three requirements: unique key, direct child, correct wrapper placement.

  4. Layout Animation Jank: Large layout shifts can cause jank even with the layout prop. Break complex layouts into smaller animated sections. Use LayoutGroup to coordinate animations across components that don’t share a parent.

  5. Reduced Motion Breaking Animations: When reducedMotion="user" is set, transform animations are disabled. If your animation relies entirely on transforms, it may appear to do nothing. Always include opacity changes as a fallback.

  6. Route Transition Conflicts: Page transitions with AnimatePresence mode="wait" can feel slow because the old page must fully exit before the new one enters. Consider mode="popLayout" or crossfade patterns for snappier transitions.

  7. Memory Leaks with Scroll Animations: Scroll-linked animations that don’t clean up properly can cause memory issues. Use Framer Motion’s built-in scroll functions rather than manual scrollTop reads.

  8. Mobile Performance Issues: Animations smooth on desktop may stutter on mobile. Test on real devices, reduce animation complexity, and avoid large blur values.

Mini Case Study: Toast Notification System

Problem: Building a toast system where notifications stack, can be dismissed individually, and animate smoothly when others are removed.

Approach: Use AnimatePresence with mode="popLayout" and the layout prop on each toast. This ensures remaining toasts animate into their new positions when one is dismissed.

import { motion, AnimatePresence } from "framer-motion";

interface Toast {
  id: string;
  message: string;
  type: "success" | "error" | "info";
}

export function ToastContainer({ 
  toasts, 
  onDismiss 
}: { 
  toasts: Toast[];
  onDismiss: (id: string) => void;
}) {
  return (
    <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
      <AnimatePresence mode="popLayout">
        {toasts.map((toast) => (
          <motion.div
            key={toast.id}
            layout
            initial={{ opacity: 0, y: 50, scale: 0.9 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
            className={`px-4 py-3 rounded-lg shadow-lg min-w-[280px] ${
              toast.type === "success" ? "bg-green-500" :
              toast.type === "error" ? "bg-red-500" : "bg-blue-500"
            } text-white`}
          >
            <div className="flex justify-between items-center">
              <span>{toast.message}</span>
              <button 
                onClick={() => onDismiss(toast.id)}
                className="ml-4 opacity-70 hover:opacity-100"
              >
                ×
              </button>
            </div>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

Tradeoffs: The layout prop adds slight overhead for position calculations. For very high-frequency toast updates, consider debouncing or limiting the maximum visible toasts.

Mini Case Study: Dashboard Sidebar with Content Reflow

Problem: A collapsible sidebar that, when toggled, should smoothly animate while the main content area expands to fill the space.

Approach: Use LayoutGroup to coordinate animations between the sidebar and content area, even though they’re separate components.

import { motion, LayoutGroup } from "framer-motion";
import { useState } from "react";

export function DashboardLayout({ children }: { children: React.ReactNode }) {
  const [sidebarOpen, setSidebarOpen] = useState(true);

  return (
    <LayoutGroup>
      <div className="flex min-h-screen">
        <motion.aside
          layout
          animate={{ width: sidebarOpen ? 256 : 64 }}
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
          className="bg-gray-900 text-white overflow-hidden"
        >
          <motion.div layout="position" className="p-4">
            <button 
              onClick={() => setSidebarOpen(!sidebarOpen)}
              className="w-full text-left"
            >
              {sidebarOpen ? "← Collapse" : "→"}
            </button>
          </motion.div>
          {sidebarOpen && (
            <motion.nav
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              className="p-4"
            >
              {/* Navigation items */}
            </motion.nav>
          )}
        </motion.aside>
        
        <motion.main layout className="flex-1 p-8">
          {children}
        </motion.main>
      </div>
    </LayoutGroup>
  );
}

Tradeoffs: LayoutGroup synchronizes layout measurements across components, which adds overhead. Only use it when components genuinely affect each other’s layout.

Do/Don’t List

DoDon’t
Animate transform and opacity for best performanceAnimate width, height, or margin directly
Use MotionConfig reducedMotion="user" globallyIgnore accessibility preferences
Extract variants outside componentsDefine animation objects inline in render
Test on real mobile devicesAssume desktop performance equals mobile

Field Notes: Performance

  • What works: Sticking to transform and opacity animations handles 95% of use cases with excellent performance. The layout prop is remarkably efficient for what it accomplishes.
  • What breaks: Animating CSS variables, especially inherited ones, can force style recalculations across thousands of elements. Large blur values (>10px) are expensive. Animating many elements simultaneously without virtualization causes frame drops.
  • How to fix it: Profile with Chrome DevTools Performance tab. Look for long “Recalculate Style” or “Layout” entries. Replace CSS variable animations with targeted style updates. Use will-change sparingly and only on elements about to animate.
  • Practical recommendation: Create a performance budget: no animation should cause frame drops on a mid-range phone. Test with CPU throttling enabled in DevTools.

Common Errors and Fixes

Quick reference for the most common Framer Motion issues.

”Cannot read property ‘getContext’ of null” / createContext Error

Cause: Missing "use client" directive in Next.js App Router.

Fix: Add "use client" at the top of any file importing from framer-motion.

Exit Animation Not Playing

Checklist:

  1. AnimatePresence wraps the conditional (not inside it)
  2. ✅ Motion component has a unique key prop
  3. ✅ Motion component is a direct child of AnimatePresence

Layout Animation Causes Content to Distort

Fix: Use layout="position" on text and images that shouldn’t scale:

<motion.div layout>
  <motion.img layout="position" src="/avatar.jpg" />
  <motion.h3 layout="position">Title</motion.h3>
</motion.div>

Hydration Mismatch Warning

Fix: Add suppressHydrationWarning or delay animation until after mount:

<motion.div layout suppressHydrationWarning />

Variant Inheritance Not Working

Cause: Child has explicit animate prop, breaking inheritance.

Fix: Remove animate from children; only define variants.

Animation Janky on Mobile

Fix:

  1. Only animate transform and opacity
  2. Keep spring stiffness 100-500, damping 10-40
  3. Avoid blur values above 10px
  4. Test with Chrome DevTools CPU throttling (4x)

useReducedMotion Returns Null

Fix: Handle the null case or use MotionConfig:

const shouldReduceMotion = useReducedMotion();
const duration = shouldReduceMotion ?? false ? 0.1 : 0.4;

Quick Reference

Essential Imports

// Core
import { motion, AnimatePresence } from "framer-motion";

// Hooks - Values & Transforms
import { useMotionValue, useTransform, useSpring, useVelocity } from "framer-motion";

// Hooks - Scroll & View
import { useScroll, useInView } from "framer-motion";

// Hooks - Animation Control
import { useAnimate, useAnimationControls, useReducedMotion } from "framer-motion";

// Optimization
import { LazyMotion, domAnimation, domMax, m } from "framer-motion";

// Layout
import { LayoutGroup, Reorder } from "framer-motion";

// Config
import { MotionConfig } from "framer-motion";

// Utilities
import { animate, stagger } from "framer-motion";

Core Props

PropTypeDescription
initialobject | string | falseStarting animation state
animateobject | stringTarget animation state
exitobject | stringExit animation (requires AnimatePresence)
transitionobjectAnimation timing/physics config
variantsobjectNamed animation states
layoutboolean | “position” | “size”Enable layout animations
layoutIdstringShared element transitions
whileHoverobjectAnimation on hover
whileTapobjectAnimation on press
whileInViewobjectAnimation when in viewport
whileDragobjectAnimation while dragging
whileFocusobjectAnimation when focused
dragboolean | “x” | “y”Enable dragging
dragConstraintsobject | RefObjectDrag boundaries
onAnimationCompletefunctionCallback when animation finishes

Hooks Reference

HookPurposeReturns
useMotionValue(initial)Create animatable value outside React stateMotionValue
useTransform(value, input, output)Derive one motion value from anotherMotionValue
useSpring(value, config)Add spring physics to a motion valueMotionValue
useVelocity(value)Track velocity of a motion valueMotionValue
useScroll(options)Track scroll progress{ scrollX, scrollY, scrollXProgress, scrollYProgress }
useInView(ref, options)Detect element visibilityboolean
useAnimate()Imperative animation control[scope, animate]
useAnimationControls()Programmatic animation triggersAnimationControls
useReducedMotion()Check user’s motion preferenceboolean | null

Transition Types

// Spring (default for physical properties)
transition={{ type: "spring", stiffness: 300, damping: 25, mass: 1 }}

// Tween (duration-based)
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
transition={{ type: "tween", duration: 0.5, ease: [0.43, 0.13, 0.23, 0.96] }} // Custom bezier

// Inertia (for drag momentum)
transition={{ type: "inertia", velocity: 50, power: 0.8 }}

// Keyframes
animate={{ x: [0, 100, 50] }}
transition={{ duration: 1, times: [0, 0.5, 1], ease: "easeInOut" }}

// Stagger (with animate function)
animate("li", { opacity: 1 }, { delay: stagger(0.1) })

AnimatePresence Modes

ModeBehaviorUse Case
"sync" (default)Enter and exit run simultaneouslyCrossfade effects
"wait"Exit completes before enter startsPage transitions
"popLayout"Exiting elements removed from layout flowList item removal

Performance Tiers

TierPropertiesPerformance
S-Tieropacity, transform (x, y, scale, rotate)GPU compositor, 60fps
A-Tierfilter (small values), clipPathPaint only, usually smooth
C-TierbackgroundColor, borderRadiusPaint, may cause jank
D-Tierwidth, height, margin, paddingLayout + Paint, avoid

Key Takeaways

  1. Replace <div> with <motion.div> and add initial, animate, transition props
  2. Use variants for orchestrated animations across parent-child components with staggerChildren
  3. AnimatePresence requires three things: wrap the conditional, unique key, direct child
  4. Layout prop enables automatic FLIP animations. Use layout="position" for text/images
  5. Stick to transform and opacity for 60fps performance on mobile
  6. Use MotionConfig reducedMotion="user" for automatic accessibility compliance
  7. LazyMotion cuts bundle size in half. Use domAnimation (about 15kb) for most apps

What to Build Next

Now that you understand Framer Motion’s core patterns, try building:

  1. A toast notification system with stacking, dismissal, and smooth reordering (uses AnimatePresence mode="popLayout" + layout)
  2. A shared element transition between a grid and detail view (uses layoutId)
  3. A scroll-driven parallax hero with multiple layers moving at different speeds (uses useScroll + useTransform)

Each project exercises different Framer Motion capabilities and will solidify your understanding of when to reach for each tool.


Version Compatibility

Framer Motion was rebranded to “Motion” in late 2024, though the npm package remains framer-motion. Here’s what you need to know:

VersionReact SupportKey FeaturesNotes
v12.x (current)React 18-19Full feature set, improved performanceRecommended for new projects
v11.xReact 18Stable, production-readySafe upgrade from v10
v10.xReact 18useAnimate, improved LazyMotionMinimum for this guide
v6-9React 17-18Core features, older APILegacy support

Migration notes:

  • v10+ requires React 18 for full compatibility
  • The motion.dev domain hosts the new documentation
  • Import paths remain unchanged (framer-motion)
  • Most v6+ code works in v12 with minimal changes

Check your version: npm list framer-motion

Frequently Asked Questions

What is Framer Motion and why should I use it?
Framer Motion is a production-ready animation library for React that uses a declarative API. It simplifies complex animations with features like variants for orchestration, AnimatePresence for exit animations, and automatic layout animations. Unlike CSS keyframes, you define animations as JavaScript objects, making them easier to coordinate with React state.
How do I handle exit animations in Framer Motion?
Wrap conditionally rendered components with AnimatePresence and add an exit prop to your motion component. The component must have a unique key prop. AnimatePresence keeps the component in the DOM until the exit animation completes. Common issues include missing keys, placing AnimatePresence inside the conditional, or having non-motion children.
What causes layout animation jank in Framer Motion?
Layout jank typically occurs when animating properties that trigger browser layout recalculations. Framer Motion's layout prop uses transform under the hood for performance, but issues arise with display:inline elements, missing re-renders, or animating within scrollable containers without layoutScroll. Use layout='position' for aspect ratio changes.
How do I make Framer Motion animations accessible?
Use MotionConfig with reducedMotion='user' to automatically disable transform and layout animations for users with reduced motion preferences. For custom control, use the useReducedMotion hook to conditionally swap heavy animations for simple opacity fades. Always test with prefers-reduced-motion enabled.
Does Framer Motion work with Next.js and SSR?
Yes, but motion components require client-side rendering. Add 'use client' directive to files using Framer Motion in Next.js App Router. Hydration mismatches can occur with layout animations. The data-projection-id prop may differ between server and client. Wrap initial animations in useEffect or use suppressHydrationWarning when needed.
How can I improve Framer Motion performance?
Animate only transform and opacity properties when possible. They run on the compositor thread without triggering layout or paint. Avoid animating width, height, or CSS variables. Use React.memo to prevent unnecessary re-renders, extract motion components, and keep spring damping/stiffness values reasonable. For large lists, consider virtualization.
How do I animate height auto in Framer Motion?
Animate directly to height: 'auto'. Framer Motion handles it natively. Wrap in AnimatePresence with initial={{ height: 0 }}, animate={{ height: 'auto' }}, exit={{ height: 0 }}. Add overflow: hidden to prevent content showing during animation. Works for accordions, expandable cards, and collapsible sections.
How do I animate SVG with Framer Motion?
Use motion.svg and motion.path components. Framer Motion supports pathLength, pathOffset, and standard transforms. Animate pathLength from 0 to 1 for drawing effects. For complex SVG morphing between different paths, use GSAP's MorphSVG plugin instead. Framer Motion doesn't support path morphing natively.
How do I disable Framer Motion animations in tests?
Wrap your test render in MotionConfig with reducedMotion='always' to make all animations instant. This prevents flaky tests from timing issues. Create a custom render function in your test utils that includes this wrapper. For tests verifying animation behavior, use reducedMotion='never' instead.
What's the difference between useAnimate and variants?
Variants are declarative. You define states and let Framer Motion handle transitions based on React state changes. useAnimate is imperative. You manually trigger animation sequences, useful for async operations or complex choreography that doesn't map cleanly to state. Use variants for 90% of cases.
How do I animate based on scroll position?
Use the useScroll hook to get motion values representing scroll progress, then useTransform to map scroll position to animation values. For simple animate when visible effects, use whileInView or useInView instead. They're simpler and more performant.
Framer Motion not working in production build?
Common causes: Tree-shaking removed features (ensure correct feature bundle with LazyMotion), CSS conflicts overriding transform or opacity, hydration mismatch (add suppressHydrationWarning), or missing 'use client' directive in Next.js App Router. Check browser console for warnings.

Sources & References