What Is Framer Motion? The React Animation Library AI Reaches For

When you tell AI to "make it feel polished," this is the library it reaches for. Here's what every piece actually does — and what breaks when it gets it wrong.

TL;DR

Framer Motion is a React animation library that turns static components into smooth, living interfaces. When your AI adds motion.div, animate, variants, or AnimatePresence to your code, it's using Framer Motion to create entrance animations, hover effects, page transitions, and exit animations. Think of it as the trim carpentry of web development — the finishing touches that make a project feel professional instead of just functional.

Why AI Coders Need to Know This

Here's a pattern every vibe coder recognizes. You've built something functional — a dashboard, a landing page, a settings panel. It works. But it feels flat. Lifeless. Like a house with drywall up but no trim, no paint, no finishing touches.

So you tell your AI: "Make this feel more polished" or "Add some smooth animations."

And suddenly your code is full of imports you've never seen — motion, AnimatePresence, useAnimation — and your regular <div> elements have turned into <motion.div> tags carrying props like initial, animate, whileHover, and variants.

The app looks fantastic. Elements fade in, cards lift on hover, panels slide in smoothly. But then something breaks. An animation stutters. A modal disappears without fading out. The page feels sluggish on a phone. And you're stuck, because you can't tell your AI what's wrong. You can't describe the problem. You just keep regenerating code and hoping.

That's what this article fixes. Here's why Framer Motion knowledge pays off specifically for vibe coders:

  • AI reaches for Framer Motion constantly. "Add animations," "make it feel smooth," "polish the UI" — all of these reliably produce Framer Motion code. If you don't recognize it, you can't debug it.
  • AI has specific, repeatable blind spots. The same three or four mistakes show up in nearly every AI-generated animation codebase. Knowing them in advance saves hours.
  • The vocabulary matters for prompting. If you can say "the exit animation isn't playing because AnimatePresence is missing," your AI fixes it in one shot. If you say "the animation doesn't work," you get a coin toss.
  • Framer Motion is genuinely readable once you know the pattern. Unlike some libraries where you need to trace through layers of abstraction, Framer Motion code says what it does. whileHover={{ scale: 1.05 }} means "scale up 5% when hovered." You can read that.

You don't need to become an animation engineer. You need to be the general contractor who knows what quality trim work looks like — so you can tell the AI when it's done the job right and when it's cut corners.

Real Scenario

Your prompt to the AI

"I have a React dashboard with stat cards, a sidebar nav, and a notifications panel. It works but feels static and cheap. Make it feel premium — smooth entrance animations when the page loads, hover effects on the cards, and animate the notification panel sliding in from the right when someone clicks the bell icon."

This is one of the most common prompts vibe coders write. And it's a perfectly good prompt — your AI knows exactly what you mean, and Framer Motion is its go-to tool for the job.

The AI will produce three categories of changes:

  1. Entrance animations — components that fade and slide up when they first appear on screen
  2. Interactive states — cards that lift and cast a shadow when you hover them
  3. Conditional transitions — the notifications panel that slides in from the right and slides back out when dismissed

Each of these uses different parts of the Framer Motion API. Understanding what each part does makes the difference between being able to direct the AI and being at its mercy.

Think of it this way: when a general contractor tells the trim carpenter to "make it look finished," they still understand what trim carpentry is. They know the difference between crown molding and baseboards. They can tell when a miter joint is sloppy. That's the level of understanding we're building here — enough to know quality work when you see it, and enough to describe problems when you don't.

What AI Generated

Here's what Cursor or Claude will typically produce after that prompt. Let's walk through each piece of code before explaining what it does.

The Import Line

// At the top of any component that uses animations
import { motion, AnimatePresence } from "framer-motion";

This pulls in two things: motion (which gives you animatable versions of HTML elements) and AnimatePresence (which handles exit animations). That's it — two imports that unlock the entire animation system.

Entrance Animations on the Stat Cards

// components/StatCard.jsx
import { motion } from "framer-motion";

function StatCard({ label, value }) {
  return (
    // motion.div is a regular div — but with animation superpowers
    <motion.div
      className="stat-card"
      initial={{ opacity: 0, y: 20 }}   // start: invisible, 20px below
      animate={{ opacity: 1, y: 0 }}    // end: fully visible, in position
      transition={{ duration: 0.4, ease: "easeOut" }} // how to get there
    >
      <h3>{label}</h3>
      <p className="stat-value">{value}</p>
    </motion.div>
  );
}

export default StatCard;

A motion.div is still a real <div> in your HTML. It still takes className, onClick, and every other prop you'd normally use. But it also understands animation props. When this component mounts, it starts invisible and 20 pixels below its natural position, then smoothly moves to fully visible and in place over 0.4 seconds.

Hover and Tap Effects

// The same StatCard, now with interactive states
<motion.div
  className="stat-card"
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.4, ease: "easeOut" }}

  // Hover: lift the card and add a deeper shadow
  whileHover={{ scale: 1.03, boxShadow: "0 10px 30px rgba(0,0,0,0.15)" }}

  // Tap/click: slight compression to feel responsive
  whileTap={{ scale: 0.98 }}
>
  <h3>{label}</h3>
  <p className="stat-value">{value}</p>
</motion.div>

whileHover and whileTap define what the element looks like during those interactions. When the cursor leaves or the tap ends, the element automatically snaps back to its animate state. No cleanup code, no event handlers to manage, no manual state to track. Framer Motion handles the whole lifecycle.

Staggered Card Entrance with Variants

// Define the animation "playbook" outside your component
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1 // each child starts 0.1s after the previous
    }
  }
};

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

// In your dashboard component
function Dashboard({ stats }) {
  return (
    // Parent container uses the container variants
    <motion.div
      className="card-grid"
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {stats.map(stat => (
        // Children inherit the current variant state from the parent
        <motion.div
          key={stat.id}
          variants={cardVariants}
          className="stat-card"
        >
          <h3>{stat.label}</h3>
          <p>{stat.value}</p>
        </motion.div>
      ))}
    </motion.div>
  );
}

Notice that the individual cards don't have initial or animate — they inherit those from the parent via the variant name propagation. The parent handles the orchestration. staggerChildren: 0.1 creates that cascading effect where each card fades up 100 milliseconds after the one before it. That's the difference between everything popping on at once and a choreographed entrance that feels considered and intentional.

The Notifications Panel Sliding In and Out

// components/NotificationsPanel.jsx
import { motion, AnimatePresence } from "framer-motion";

function Dashboard() {
  const [showNotifications, setShowNotifications] = useState(false);

  return (
    <div>
      <button onClick={() => setShowNotifications(!showNotifications)}>
        Bell
      </button>

      {/* AnimatePresence MUST wrap any conditionally rendered motion component */}
      <AnimatePresence>
        {showNotifications && (
          <motion.div
            key="notifications"          // key is required for AnimatePresence
            className="notifications-panel"
            initial={{ x: 300, opacity: 0 }}  // start off-screen right
            animate={{ x: 0, opacity: 1 }}    // slide to visible position
            exit={{ x: 300, opacity: 0 }}     // slide back off-screen on close
            transition={{ type: "spring", damping: 25, stiffness: 200 }}
          >
            <h2>Notifications</h2>
            {/* notification items */}
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

This is the most important pattern in Framer Motion — and the one AI gets wrong most often. In React, when showNotifications flips to false, the component is yanked from the page instantly. No goodbye. Just gone. AnimatePresence intercepts that removal: "Hold on, React. Let the exit animation play first, then remove it." Without AnimatePresence, the exit prop does absolutely nothing.

Understanding Each Part

Every Framer Motion animation boils down to the same small set of props. Once you recognize them, you can read any AI-generated animation code.

The motion.* component family

Framer Motion wraps every standard HTML element: motion.div, motion.span, motion.button, motion.ul, motion.li, motion.section — whatever element you need, prepend motion. to it. The wrapped version accepts all the same props as the original plus the animation props described below. It still renders as the same HTML element. The browser sees a normal <div>; Framer Motion controls how it moves.

initial — the starting state

Defines where the element starts before it appears. initial={{ opacity: 0, y: 20 }} means "start invisible and 20 pixels below your natural position." If you omit initial, the element just jumps straight to its animate state with no transition. Think of it as setting the starting position of an actor before they walk on stage.

animate — the destination state

This is where the element ends up. Framer Motion automatically animates from initial to animate the moment the component appears on screen. animate={{ opacity: 1, y: 0 }} means "become fully visible and move to your natural layout position." The animation plays once, on mount.

exit — the departure state

The animation that plays when the component is conditionally removed from the DOM. Only works inside an AnimatePresence wrapper. Without that wrapper, this prop is completely inert — it looks like it should work, and it doesn't, and no error gets thrown. This is the silent failure mode that trips up most people.

transition — the physics of movement

Controls the timing and feel of the animation. You'll see two main flavors in AI-generated code:

  • Tween: {{ duration: 0.4, ease: "easeOut" }} — a fixed-duration animation, like CSS transitions. Clean, predictable, works great for fades and subtle slides.
  • Spring: {{ type: "spring", stiffness: 300, damping: 20 }} — physics-based animation that simulates a real spring. No set duration — it runs until the "spring" settles. Higher stiffness = snappier. Higher damping = less bouncy. This is what makes UI feel "alive" instead of mechanical.

When your AI uses type: "spring", it's deliberately choosing to make the motion feel natural and physical rather than robotic and clock-based. That's usually the right call for interactive elements like modals, drawers, and hover states.

whileHover and whileTap — interaction states

whileHover defines what the element looks like while the cursor is over it. whileTap handles click and touch press states. When the interaction ends, the element automatically returns to its animate state — no event handlers, no state variables, no cleanup. This is where Framer Motion saves you the most boilerplate compared to CSS hover states that require class toggling and transitions.

variants — the named animation playbook

Instead of writing identical animation values on every element in a list, you define a variants object with named states, then reference those states by name. Two important behaviors come from variants:

  1. Children inherit the parent's current variant. When a parent is in the "visible" state, all children with a "visible" variant automatically animate to that state too — no wiring required.
  2. Parents can orchestrate children's timing. staggerChildren, delayChildren, and when properties on a parent's transition control when and how children animate, creating coordinated sequences from a single source of truth.

layout — animate position changes automatically

// Add layout to animate position changes automatically
<motion.div layout className="card">
  {content}
</motion.div>

Add the layout prop to a component and Framer Motion will smooth out any position or size change — whether from filtering a list, sorting items, a window resize, or a sibling being removed. Without layout, elements jump. With it, they glide. This is what makes filter/sort interfaces feel polished instead of jarring, and it takes exactly one word to add.

A note on naming: framer-motion vs. motion

If you see AI importing from "motion/react" instead of "framer-motion", don't panic. In late 2024, the library was rebranded. The framer-motion package still works; the newer motion package is the same library with a new name and smaller bundle size. Same API, same concepts, everything in this article applies to both. If your AI mixes both imports in the same project, that's a bug — tell it to pick one and be consistent.

What AI Gets Wrong About This

AI is genuinely good at writing Framer Motion code. But it has consistent, predictable blind spots. Knowing these in advance saves hours of "why isn't this working" frustration.

1. Missing AnimatePresence on exit animations

This is the number-one bug. Your AI adds a beautiful exit prop to a conditional component — a modal that should fade out, a toast that should slide away, a dropdown that should collapse — but forgets to wrap the conditional block in <AnimatePresence>.

The result: entrance animations work perfectly, but when the component is removed, it just disappears. No exit animation. No smooth goodbye. And crucially, no error message — the code runs fine, it just silently ignores the exit prop.

Also check: every direct child of AnimatePresence needs a unique key prop. Without it, AnimatePresence can't tell which children are entering and which are exiting.

// ❌ Exit animation silently ignored
<div>
  {showModal && (
    <motion.div exit={{ opacity: 0 }}>
      Modal content
    </motion.div>
  )}
</div>

// ✅ Exit animation plays correctly
<AnimatePresence>
  {showModal && (
    <motion.div key="modal" exit={{ opacity: 0 }}>
      Modal content
    </motion.div>
  )}
</AnimatePresence>

Prompt to fix: "The exit animation on [component name] isn't playing. Wrap the conditional render in AnimatePresence and make sure the motion component has a unique key prop."

2. Animating layout-triggering properties (performance kills)

Not all CSS properties are created equal when it comes to animation. Animating width, height, top, left, margin, or padding forces the browser to recalculate the layout of your entire page on every animation frame. This is expensive. On a modern desktop you won't notice. On a phone, or with more than a handful of animated elements, you'll start seeing jank.

AI often does this because height: 0 to height: "auto" seems like the obvious way to animate an accordion or collapsible panel. It works — just poorly at scale.

// ❌ Triggers expensive browser relayout on every frame
<motion.div
  initial={{ height: 0, width: 200 }}
  animate={{ height: 300, width: 400 }}
/>

// ✅ GPU-accelerated, smooth at 60fps
<motion.div
  initial={{ opacity: 0, scale: 0.8 }}
  animate={{ opacity: 1, scale: 1 }}
/>

The safe list: animate opacity, x, y, scale, rotate, skew. These are all transform-based and run on the GPU. Everything else triggers layout recalculations.

Prompt to fix: "The animations feel janky on mobile. Replace any width/height/top/left animations with transform-based equivalents (opacity, scale, x, y). Only animate layout-triggering properties if strictly necessary."

3. CSS transitions fighting Framer Motion

Framer Motion controls animations via inline styles. If your CSS file has transition properties on the same element Framer Motion is animating, both try to run simultaneously. The result is a janky double-animation or an animation that snaps to its final position instead of easing into it.

This happens most often when AI adds Framer Motion to components that already had CSS hover effects — the CSS transition: transform 0.2s and Framer Motion's spring both try to control transform at the same time.

/* ❌ This CSS transition will fight Framer Motion */
.stat-card {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

/* ✅ Remove CSS transitions from motion-controlled elements */
.stat-card {
  /* Let Framer Motion handle all animation */
}

Prompt to fix: "There's a janky double-animation on the cards. Find any CSS transition properties on elements that Framer Motion is also animating and remove them — Framer Motion should have exclusive control."

4. Animating too many elements simultaneously

AI loves to be thorough. Ask for "smooth animations" and it might animate every element on the page — 50 list items all with individual entrance animations, layout animations on every card in a 200-item grid, spring physics on every hover state everywhere. On a laptop this looks great. On a phone, you've built a slideshow.

Best practices for keeping animations performant:

  • Limit staggered entrance animations to 10–15 items maximum. Beyond that, only animate items in the current viewport.
  • Use layout sparingly on complex pages. Layout animations measure the entire DOM, which gets expensive with deep component trees.
  • Don't add whileHover to every interactive element. Save the spring physics for primary actions — buttons, cards, the important interactive surfaces.

Prompt to fix: "The page is slow on mobile. Limit entrance animations to the first 12 items, remove layout animations from nested components, and only use spring physics on the primary action cards."

5. Forgetting to install the package

This sounds obvious, but it's remarkably common. AI writes complete, correct Framer Motion code without checking whether the package is in your package.json. You paste it in, the import fails, and you get a module not found error.

# Install whichever package your AI is importing from
npm install framer-motion
# or the rebranded package
npm install motion

If you see Cannot find module 'framer-motion', that's why. Run the install, restart your dev server, and you're fine.

Prompt to prevent this: "Before writing the animation code, check my package.json and tell me if I need to install framer-motion or motion first."

How to Debug with AI

When Framer Motion animations misbehave, here's exactly what to tell your AI. The more specific your description, the faster the fix.

Problem: Exit animation doesn't play — component just disappears

What's happening: Either AnimatePresence is missing, or the animated component isn't a direct child of AnimatePresence (wrapping it in an extra div breaks it), or the key prop is missing.

Debugging prompt: "The exit animation on [component name] isn't playing — it just vanishes. Please check: 1) is AnimatePresence wrapping the conditional render? 2) is the motion component a direct child? 3) does it have a unique key prop? Fix all three if any are wrong."

Problem: Animations stutter or feel slow on mobile

What's happening: You're probably animating layout-triggering properties (width, height, top, left), running too many simultaneous animations, or there's a CSS transition conflict.

Debugging prompt: "Animations feel janky on mobile. Check for: 1) any width/height/top/left animations that should use transform instead, 2) more than 15 elements animating at once, 3) CSS transition properties on any element Framer Motion is controlling. Report what you find and fix it."

Problem: Animation plays on enter but not on navigate back

What's happening: In some React Router or Next.js setups, navigating back to a page reuses the component without unmounting it. Framer Motion's initial/animate cycle only fires on mount. If the component never unmounts, the animation never replays.

Debugging prompt: "The entrance animation plays the first time but not when I navigate back to this page. I'm using [React Router / Next.js App Router]. How do I make the animation replay on every navigation? Should I use useAnimation hook or add a key to the page component?"

Problem: Cannot read properties of undefined (reading 'stiffness')

What's happening: The transition object is malformed — usually a typo in a property name or a missing type declaration. type: "spring" needs to be present if you're using spring-specific properties like stiffness and damping.

Debugging prompt: "Getting this error: [paste full error]. Here's the motion component code: [paste code]. Check the transition object for malformed properties."

Slow-motion trick for any animation bug

Whenever an animation looks wrong but you can't tell why, temporarily add transition={{ duration: 3 }} to slow it down to three seconds. This reveals exactly where it starts, where it ends, and what path it takes — making any problem obvious. When it's fixed, remove the override.

// Temporary debugging: slow the animation way down
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 3 }}  // <-- add this, check what happens, remove
>
  content
</motion.div>

Also worth checking: the React Developer Tools browser extension. Find your motion.div in the component tree, inspect its props, and verify that initial, animate, and exit have the values you expect. Typos in variant names fail silently — a variant named "visiible" will just never trigger.

Quick Reference

Prop / Concept What It Does Common Value
motion.div Animatable version of a regular <div> Replace any <div> you want to animate
initial Starting state before the component appears { opacity: 0, y: 20 }
animate Target state — where it ends up on mount { opacity: 1, y: 0 }
exit State to animate to before unmounting { opacity: 0, x: 300 }
transition Controls timing and physics of the animation { type: "spring", damping: 25 }
whileHover State while cursor is over the element { scale: 1.05 }
whileTap State while element is being pressed { scale: 0.97 }
variants Named animation states; children inherit parent's current variant { hidden: ..., visible: ... }
staggerChildren Delay between each child's animation start (inside transition) 0.1 (100ms between each)
layout Smoothly animate position/size changes Just the word layout as a boolean prop
AnimatePresence Required wrapper for exit animations to work Wrap any conditional motion.* component
key on child of AnimatePresence Tells AnimatePresence which element is entering/exiting Any stable unique string, e.g. "modal"
Safe properties to animate GPU-accelerated, won't cause jank opacity, x, y, scale, rotate
Avoid animating Triggers layout recalculation — slow on mobile width, height, top, left, margin

What to Learn Next

Framer Motion is one layer of the React UI stack. These articles give you the surrounding context:

  • What Is React? — Framer Motion is built entirely around React's component lifecycle. Understanding components, state, and conditional rendering makes AnimatePresence and variants click into place.
  • What Is CSS? — Framer Motion builds on top of CSS transforms and opacity. Knowing what CSS can and can't do on its own tells you exactly when you need a library and when you don't.
  • What Is Zustand? — Animations in Framer Motion are driven by React state. Zustand is how AI manages the state that triggers those animations — open/closed modals, visible/hidden panels, active/inactive tabs.
  • What Is Tailwind CSS? — Framer Motion handles motion; Tailwind handles the static design system. They work together constantly in AI-generated UIs. Understanding both makes reading the generated code much easier.
  • What Is shadcn/ui? — The component library AI reaches for alongside Framer Motion. shadcn/ui gives you the structure and styling; Framer Motion gives it life.

Key Insight

Framer Motion doesn't do anything that CSS transitions can't conceptually do — it just does it with React state instead of CSS classes. The reason AI loves it isn't because it's magic. It's because animate={{ opacity: 1 }} is easier to generate correctly than managing CSS class toggles, JavaScript timing, and transition event listeners by hand. Once you see it that way, every prop becomes readable.

FAQ

Framer Motion is a React animation library that lets you add smooth transitions, entrance effects, hover states, and exit animations to components. AI coding tools reach for it because it has a simple, declarative API — you describe what the animation should look like, and Framer Motion handles the math. It's the most popular React animation library, with over 25 million npm downloads per month as of 2025. Less code to generate means fewer places for the AI to make mistakes, which is exactly why it defaults to Framer Motion over manual CSS animation approaches.

A regular div is static — it just sits there. A motion.div is a supercharged version that Framer Motion can animate. It accepts special props like initial, animate, exit, whileHover, and transition that tell it how to move, fade, scale, or transform. Under the hood it's still a div in your HTML — the browser doesn't see anything different. Framer Motion adds the animation behavior on top, managing it through React and inline styles.

The most common reason is a missing AnimatePresence wrapper. In React, when a component is conditionally removed from the page, it's gone instantly — there's no built-in way to animate it out. AnimatePresence intercepts that removal and lets the exit animation play first. Without it, the exit prop is completely ignored — no error, just silence. Also check that the component has a unique key prop, which AnimatePresence requires to track which children are leaving.

You don't need to master it, but you should understand the basics — what motion.div does, what animate and variants mean, and how AnimatePresence works. This lets you read what your AI generated, recognize the five common mistakes before they bite you, and write targeted fix prompts instead of regenerating everything from scratch. The basics take an hour to learn and pay off every single time AI adds animations to your project.

For typical use — entrance animations, hover effects, page transitions — Framer Motion has negligible performance impact. Problems appear when you animate dozens of elements simultaneously, use layout animations on complex DOM trees, or animate properties that trigger browser repaints (like width and height instead of transform and opacity). The rule: stick to animating opacity and transform-based properties (x, y, scale, rotate) and you'll stay smooth at 60fps on any device.