How to Animate Like a Pro Using Framer Motion
#framer-motion
#react
#animation
#ux
#tutorial
How to Animate Like a Pro Using Framer Motion
Framer Motion makes it simple to build production‑ready animations in React. Beyond quick fades and slides, it gives you a full toolkit for orchestrating complex sequences, shared element transitions, scroll reveals, and micro‑interactions that feel crisp at 60fps.
This guide distills battle‑tested patterns, code snippets, and performance tips so you can animate like a pro—without fighting the browser.
Prerequisites and Setup
- A React project (Create React App, Next.js, Vite, etc.)
- Basic familiarity with JSX
Install:
npm install framer-motion
# or
yarn add framer-motion
Optional performance bundle:
import { LazyMotion, domAnimation, m } from "framer-motion";
The Building Blocks
Framer Motion exports pre‑animated elements called motion components (e.g., motion.div). You combine these with animation props to define behavior.
initial: starting stateanimate: target stateexit: leave state (used withAnimatePresence)transition: timing, easing, spring physicsvariants: reusable named states and orchestration
A Minimal Example
import { motion } from "framer-motion";
export function Hero() {
return (
<motion.h1
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
Animate like a pro
</motion.h1>
);
}
Variants and Orchestration (Professional Pattern)
Variants let parents coordinate children: stagger entrances, delay groups, or ensure a timeline.
import { motion } from "framer-motion";
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, y: 16 },
show: { opacity: 1, y: 0, transition: { duration: 0.35 } },
};
export function FeatureList() {
return (
<motion.ul variants={container} initial="hidden" animate="show">
{[1, 2, 3].map(i => (
<motion.li key={i} variants={item}>
Feature {i}
</motion.li>
))}
</motion.ul>
);
}
Keynotes:
- Use a parent container variant for staggered entrances.
when: "beforeChildren"ensures child animations start after the parent begins.
Micro‑Interactions: Hover, Tap, Focus
Use whileHover and whileTap for instant feedback. Add spring transitions for tactile feel.
import { motion } from "framer-motion";
export function PrimaryButton({ children }) {
return (
<motion.button
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 450, damping: 30 }}
className="btn"
>
{children}
</motion.button>
);
}
Tip: Reinforce with non‑motion cues (color, elevation) for accessibility and reduced‑motion users.
Animate on Scroll: useInView
Reveal content as it enters the viewport without heavy scroll listeners.
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
export function SectionReveal() {
const ref = useRef(null);
const inView = useInView(ref, { once: true, margin: "-10% 0px" });
return (
<motion.section
ref={ref}
initial={{ opacity: 0, y: 32 }}
animate={inView ? { opacity: 1, y: 0 } : undefined}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<h2>Scroll‑based reveal</h2>
<p>Smooth entrance when scrolled into view.</p>
</motion.section>
);
}
Pro tip: Use once: true to avoid repeated animations during fast scrolls.
Enter/Exit Transitions: AnimatePresence
AnimatePresence ensures exit animations run for components leaving the tree. This is essential for modals, toasts, menus, and route transitions.
import { AnimatePresence, motion } from "framer-motion";
export function Modal({ open, onClose }) {
return (
<AnimatePresence>
{open && (
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="backdrop"
>
<motion.dialog
initial={{ opacity: 0, y: 24, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 12, scale: 0.98 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
onClick={e => e.stopPropagation()}
>
<h3>Modal Title</h3>
<p>Content goes here.</p>
</motion.dialog>
</motion.div>
)}
</AnimatePresence>
);
}
Tip: Put a key on animated blocks if you need to force re‑animation on content changes.
Layout and Shared Element Transitions
Framer Motion can animate between layout states automatically.
Auto layout animations
Use the layout prop when elements change size or position.
import { motion } from "framer-motion";
import { useState } from "react";
export function ExpandableCard() {
const [open, setOpen] = useState(false);
return (
<motion.article layout className="card" onClick={() => setOpen(o => !o)}>
<motion.header layout>
<h4>Expandable Card</h4>
</motion.header>
{open && (
<motion.div layout initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<p>More details with smooth size transition.</p>
</motion.div>
)}
</motion.article>
);
}
Shared element transitions with layoutId
Animate an element seamlessly between two components or routes.
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
import { useState } from "react";
export function Gallery() {
const [active, setActive] = useState(null);
const items = [{ id: "a" }, { id: "b" }, { id: "c" }];
return (
<LayoutGroup>
<div className="grid">
{items.map(item => (
<motion.img
key={item.id}
layoutId={item.id}
src={`/img/${item.id}.jpg`}
onClick={() => setActive(item)}
/>
))}
</div>
<AnimatePresence>
{active && (
<motion.div
className="lightbox"
onClick={() => setActive(null)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.img layoutId={active.id} src={`/img/${active.id}.jpg`} />
</motion.div>
)}
</AnimatePresence>
</LayoutGroup>
);
}
Reordering and Drag
Framer Motion includes primitives for drag gestures and list reordering.
import { Reorder, motion } from "framer-motion";
import { useState } from "react";
export function ReorderableList() {
const [items, setItems] = useState(["Design", "Develop", "Deploy"]);
return (
<Reorder.Group axis="y" values={items} onReorder={setItems}>
{items.map(item => (
<Reorder.Item
key={item}
value={item}
as={motion.li}
whileDrag={{ scale: 1.02 }}
transition={{ type: "spring", stiffness: 500, damping: 35 }}
>
{item}
</Reorder.Item>
))}
</Reorder.Group>
);
}
Add dragConstraints or dragMomentum={false} if you need tighter control.
Motion Values, Springs, and Derived Animations
For dynamic physics and synchronized effects, use motion values.
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
export function DragToFade() {
const x = useMotionValue(0);
const xSpring = useSpring(x, { stiffness: 400, damping: 30 });
const opacity = useTransform(xSpring, [-120, 0, 120], [0, 1, 0]);
return (
<motion.div
drag="x"
style={{ x: xSpring, opacity }}
dragConstraints={{ left: -120, right: 120 }}
className="pill"
>
Drag me
</motion.div>
);
}
Performance Techniques
- Prefer transforms over layout properties:
- Animate
opacity,transform(translate/scale/rotate) instead oftop/left/width/heightwhen possible.
- Animate
- Use LazyMotion to tree‑shake features:
import { LazyMotion, domAnimation, m } from "framer-motion";
export function App() {
return (
<LazyMotion features={domAnimation}>
<m.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
Content
</m.div>
</LazyMotion>
);
}
- Keep shadows, blurs, and large box‑filters static during animations; animate wrapper transforms instead.
- Batch animations with variants to minimize reflows.
- For large lists, avoid animating every item simultaneously; stagger or virtualize.
Accessibility and Reduced Motion
Respect user preferences and provide non‑motion affordances.
Global config:
import { MotionConfig } from "framer-motion";
export function Root({ children }) {
return (
<MotionConfig reducedMotion="user">
{children}
</MotionConfig>
);
}
Conditional transitions:
import { useReducedMotion } from "framer-motion";
export function FadeIn({ children }) {
const prefersReduced = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={prefersReduced ? { duration: 0 } : { duration: 0.4 }}
>
{children}
</motion.div>
);
}
Also:
- Do not rely on motion alone to signal state changes.
- Maintain keyboard focus order during animations.
- Ensure reduced‑motion users still get context via instant state changes.
Common Pitfalls and How to Avoid Them
- Missing
exitstates: Components pop out instantly. Wrap withAnimatePresenceand defineexit. - Animating layout props unnecessarily: Causes reflow. Prefer transforms or
layoutfor autos. - Choppy scroll reveals: Use
useInViewwithonce: trueand modest durations. - Competing animations: Centralize via variants; avoid multiple
animateprops fighting on the same element. - Route transitions: Keep
AnimatePresenceat the layout level and set uniquekeyper route.
A Polished Card Example (Putting It Together)
import {
AnimatePresence,
LazyMotion,
domAnimation,
m,
useInView,
} from "framer-motion";
import { useRef, useState } from "react";
const card = {
hidden: { opacity: 0, y: 24 },
show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } },
exit: { opacity: 0, y: 12, transition: { duration: 0.25 } },
};
export function PolishedCard() {
const [open, setOpen] = useState(false);
const ref = useRef(null);
const inView = useInView(ref, { once: true, margin: "-15% 0px" });
return (
<LazyMotion features={domAnimation}>
<m.article
ref={ref}
variants={card}
initial="hidden"
animate={inView ? "show" : "hidden"}
exit="exit"
className="card"
onClick={() => setOpen(o => !o)}
whileHover={{ y: -2 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
<m.header layout>
<h3>Pro Card</h3>
<p>Hover, reveal, expand</p>
</m.header>
<AnimatePresence initial={false}>
{open && (
<m.div
key="body"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<p>Extra details with auto layout and presence.</p>
</m.div>
)}
</AnimatePresence>
</m.article>
</LazyMotion>
);
}
Final Checklist for Pro‑Level Motion
- Define a motion system: durations, easings, and spring presets.
- Use variants for orchestration and reusability.
- Respect reduced motion; provide alternative cues.
- Prefer transforms; avoid animating costly properties.
- Use LazyMotion and keep animations purposeful and restrained.
- Test with keyboard, screen readers, and low‑end devices.
With these patterns and practices, your interfaces will feel fast, intentional, and delightful—without adding complexity to your codebase.