MervCodes

Tech Reviews From A Programmer

Web Animation With Framer Motion: React Guide

1 min read

Motion is one of the fastest ways to make a React interface feel intentional. A button that eases into view, a list that reflows smoothly when an item is deleted, a modal that scales up from nothing — these small touches signal quality. The problem is that hand-writing CSS keyframes and juggling transitionend listeners gets painful the moment your animations need to respond to state, gestures, or component mount/unmount cycles.

Framer Motion (now published as the motion library) solves this by making animation a first-class property of your JSX. This guide walks through the core mental model and the features you'll actually reach for in production.

Getting Started

Install the package:

npm install framer-motion

The entire library revolves around one idea: instead of a plain <div>, you render a motion.div. That element accepts animation props that describe what state it should be in, not how to get there.

import { motion } from "framer-motion";

function FadeInBox() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4 }}
    >
      Hello, motion
    </motion.div>
  );
}

Three props do the heavy lifting:

  • initial — the state before the element appears.
  • animate — the state it moves toward, re-animating whenever these values change.
  • transition — the timing and physics of the movement.

You never write @keyframes. You declare the target and Framer Motion interpolates.

The Transition Model

By default, Framer Motion animates with a spring, not a fixed duration. Springs feel natural because they model physics — mass, stiffness, and damping — rather than a mechanical curve. You can tune them:

<motion.div
  animate={{ x: 100 }}
  transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>

If you prefer predictable timing, switch to a tween with an easing curve:

transition={{ type: "tween", duration: 0.3, ease: "easeInOut" }}

A practical rule: use springs for interactive elements (drag, hover, toggles) because they respond to velocity, and tweens for scripted, timed sequences where you need exact durations.

Animating Presence: Enter and Exit

React unmounts components immediately, which normally makes exit animations impossible — the element is gone before it can animate out. AnimatePresence fixes this by keeping an element in the DOM until its exit animation finishes.

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

function Toast({ visible }) {
  return (
    <AnimatePresence>
      {visible && (
        <motion.div
          initial={{ opacity: 0, scale: 0.8 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.8 }}
        >
          Saved!
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Two things matter here. First, the exit prop only works inside AnimatePresence. Second, when you animate a list of items, give each a stable key — that's how Framer Motion tracks which elements are entering and leaving.

Variants: Orchestrating Groups

Repeating the same initial/animate objects across many elements gets verbose. Variants let you name animation states and share them, and — critically — they let a parent orchestrate its children.

const list = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.08 },
  },
};

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

function Menu({ items }) {
  return (
    <motion.ul variants={list} initial="hidden" animate="visible">
      {items.map((text) => (
        <motion.li key={text} variants={item}>
          {text}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Notice the children don't repeat initial="hidden" — the parent propagates its state name down, and staggerChildren cascades them one after another. This is how you build those polished "list items fly in one by one" effects with almost no code.

Gestures: Hover, Tap, and Drag

Framer Motion ships gesture props that are far cleaner than wiring up event listeners:

<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
>
  Click me
</motion.button>

whileHover and whileTap apply a temporary state that automatically reverts when the gesture ends. Dragging is equally direct:

<motion.div
  drag
  dragConstraints={{ left: 0, right: 300, top: 0, bottom: 0 }}
  dragElastic={0.2}
/>

dragConstraints bounds the movement, and dragElastic controls the rubber-band resistance when the user pulls past the edge. Because drag uses spring physics, releasing mid-throw carries momentum naturally.

Layout Animations

This is Framer Motion's most impressive trick. Add the layout prop and the element automatically animates whenever its size or position changes — even when that change comes from a CSS reflow you didn't explicitly animate.

<motion.div layout>
  {expanded ? <FullContent /> : <Summary />}
</motion.div>

When expanded flips, the box smoothly resizes instead of snapping. Under the hood it uses the FLIP technique (First, Last, Invert, Play), measuring the before-and-after positions and animating the difference on the GPU.

Pair it with layoutId to animate an element across components — a thumbnail that morphs into a full-screen hero image, for instance:

{selected ? (
  <motion.img layoutId="hero" src={img} className="full" />
) : (
  <motion.img layoutId="hero" src={img} className="thumb" />
)}

Framer Motion recognizes the shared layoutId and tweens between the two positions automatically.

Scroll-Driven Animation

For effects tied to scroll position, useScroll gives you motion values you can map onto any property:

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

function Parallax() {
  const { scrollYProgress } = useScroll();
  const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
  return <motion.div style={{ y }} />;
}

scrollYProgress runs from 0 to 1 across the page, and useTransform remaps that range to a pixel offset. Because these are motion values — not React state — updates skip re-renders and stay smooth.

Performance Tips

  • Animate transform and opacity, not layout properties. Changing x, y, scale, and opacity runs on the compositor thread. Animating width, height, top, or margin forces layout recalculation on every frame.
  • Prefer layout over manual size animation. It's specifically optimized to avoid layout thrash.
  • Reach for motion values over state for high-frequency updates like scroll and drag. A useState update on every frame will drop frames.
  • Respect reduced motion. Wrap your app in <MotionConfig reducedMotion="user"> so users who set that OS preference get muted animations automatically.
  • Lazy-load for bundle size. Use the LazyMotion component with domAnimation features to ship a smaller runtime when you don't need every capability.

Putting It Together

A realistic pattern combines several of these ideas — a modal that fades its backdrop, springs its panel into view, and animates out cleanly:

<AnimatePresence>
  {open && (
    <motion.div
      className="backdrop"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      <motion.div
        className="panel"
        initial={{ scale: 0.9, y: 20 }}
        animate={{ scale: 1, y: 0 }}
        exit={{ scale: 0.9, opacity: 0 }}
        transition={{ type: "spring", stiffness: 300, damping: 30 }}
      >
        {children}
      </motion.div>
    </motion.div>
  )}
</AnimatePresence>

Start small: add a whileHover to a button, then a fade-in, then reach for variants and layout as your needs grow.

FAQ

Is Framer Motion the same as the motion package? Yes — the library was renamed to motion and is now framework-agnostic, but the React API you import from framer-motion remains fully supported and functionally identical. Existing framer-motion imports continue to work.

Do I need Framer Motion if CSS transitions exist? For simple hover states, plain CSS is fine and lighter. Framer Motion earns its place when you need exit animations, gesture physics, layout transitions, orchestrated sequences, or animations driven by React state — all of which are awkward or impossible in pure CSS.

Why isn't my exit animation running? The element must be a direct conditional child of AnimatePresence, and it needs a stable key. If you conditionally render the AnimatePresence wrapper itself, the exit never gets a chance to play.

Does it work with server-side rendering and React Server Components? Yes for SSR. Animation components are client components, so mark files using motion with "use client" in frameworks like Next.js App Router.

How big is the bundle? The full library is moderate in size, but LazyMotion with the domAnimation feature set lets you load a much smaller subset, deferring heavier features until needed.

Can I animate SVG paths? Yes. motion.path supports animating pathLength, pathOffset, and pathSpacing from 0 to 1, which is how you create those "line draws itself" effects.

Framer Motion's real strength is that it scales with you: the first animation is one prop, and the hundredth is still just a declarative description of where things should be. Get comfortable with initial, animate, and transition, then layer in presence, variants, gestures, and layout as your interface demands them.

Sources

Related Articles