Framer Motion in React: Animations That Feel Alive
Framer Motion gives React animations a physics-based feel that CSS transitions can't match. Here's how to actually use it without overcomplicating your components.
Why Framer Motion Hits Different
CSS transitions are fine. They do the job. But if you've ever watched a native iOS app animate a card into view and thought "why does my React app feel so stiff by comparison" — Framer Motion is the answer you've been looking for.
It shipped its 1.0 back in 2019 and has been the de facto animation library for React ever since. The core idea is simple: you replace your plain HTML elements with motion equivalents, then pass animation props directly. No imperative code, no timeline juggling, no ref gymnastics just to move a div 20px.
Honestly, the thing that keeps me coming back isn't the API — it's the spring physics. A CSS ease-out curve is predictable. A spring animation *feels* like something has weight. That difference is what separates UIs that users enjoy from UIs they just tolerate.
Worth noting: Framer Motion works entirely in userland. You don't need to touch your bundler, change your build config, or add a Babel plugin. npm install framer-motion and you're off.
Getting Started: The motion Component
The entry point is the motion factory. Every HTML and SVG element has a motion equivalent — motion.div, motion.span, motion.path. You swap the tag, add your props, done.
Here's the most basic example you'll actually use day-to-day:
import { motion } from 'framer-motion';
export function FadeInCard({ children }) {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
style={{
padding: '24px',
borderRadius: '12px',
background: 'white',
boxShadow: '0 4px 24px rgba(0,0,0,0.08)',
}}
>
{children}
</motion.div>
);
}Three props do all the heavy lifting here. initial is where the element starts. animate is where it ends up. exit fires when the component unmounts — but only if you wrap it in <AnimatePresence> (more on that in a moment). The 16px vertical offset on entry and exit gives it that satisfying "slides in from below" feel without being dramatic.
That said, don't just animate everything. Overusing motion components is how you end up with a UI that feels like a Vegas slot machine. Pick the interactions that *matter* — page transitions, modal entrances, success states — and let everything else be static.
AnimatePresence: The Exit Animation Problem
React unmounts components instantly. There's no DOM lifecycle hook that gives you time to play an exit animation before the element disappears. AnimatePresence solves this by keeping the element in the DOM until its exit animation completes.
import { motion, AnimatePresence } from 'framer-motion';
export function Toast({ show, message }) {
return (
<AnimatePresence>
{show && (
<motion.div
key="toast"
initial={{ opacity: 0, scale: 0.92 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.88, y: 8 }}
transition={{ type: 'spring', stiffness: 300, damping: 24 }}
className="toast"
>
{message}
</motion.div>
)}
</AnimatePresence>
);
}A couple of gotchas here. First, every direct child of AnimatePresence needs a stable key prop. Without it, React can't tell whether it's dealing with the same element or a new one, and exit animations won't fire. Second, AnimatePresence only watches its *direct* children — you can't nest it arbitrarily and expect magic.
In practice, I wrap my page-level route components with AnimatePresence and call it a day for 80% of projects. For complex list animations, that's where layout and layoutId come in — but that's a deeper topic.
Spring Physics vs Tween: When to Use Which
Framer Motion gives you two main transition types: tween (your standard duration-based easing) and spring (physics simulation with stiffness, damping, and mass). Knowing when to pick each one saves you from a lot of "why does this feel wrong" debugging.
Use tween when you need precision. Fade animations, color transitions, anything where timing matters more than feel. A 300ms easeInOut opacity fade is exactly that — 300ms, predictable, done.
Use spring for anything that involves position or scale. Moving a card, scaling a button on hover, dragging. The default spring values in Framer Motion (stiffness: 100, damping: 10) are actually a bit bouncy for most UI work. I usually start at stiffness: 260, damping: 20 for snappy-but-controlled movement, and dial from there.
Quick aside: the mass parameter is the underrated one. Higher mass means slower acceleration and deceleration, which reads as "heavier." If you're animating something physically large on screen — a sidebar, a full-width banner — bumping mass to 1.2 or 1.5 makes it feel more grounded. Small interactive elements like chips or badges? Keep mass at the default 1.
Variants: Orchestrating Complex Animations
Once you have more than two or three animated elements on screen at once, inline animation props get messy fast. Variants let you name animation states and reference them by string, then propagate them down the component tree automatically.
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 260, damping: 20 } },
};
export function AnimatedList({ items }) {
return (
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.label}
</motion.li>
))}
</motion.ul>
);
}The staggerChildren: 0.08 in the container means each child fires 80ms after the previous one. That staggered cascade is one of those small things that makes a list feel *designed* rather than just rendered.
One more thing — you don't have to define every state in every variant object. Framer Motion inherits values from parent variants, so child variants only need to override what's different. Keep your variant objects lean.
This pattern scales really well when you're building component libraries. If you're using Empire UI, variants are how most of the animated primitives handle their enter/exit states without passing a wall of props down the tree.
Framer Motion vs GSAP: The Honest Take
Look, GSAP is extraordinarily powerful. If you need scroll-based scrubbing, SVG path morphing, or timeline sequences that sync with audio — GSAP is the right tool. It's been battle-tested since 2012 and its performance floor is higher than anything else on the web.
But for 90% of React component animation work, Framer Motion wins on developer experience. It's declarative, it composes with React's mental model instead of fighting it, and you don't need to manage refs and cleanup manually. The API fits in your head.
The performance story is also more nuanced than "GSAP is faster." Framer Motion animates transform and opacity by default — the same properties that GSAP recommends for GPU-composited animations. For typical UI work, you won't notice a difference.
Where Framer Motion stumbles is complex scroll-linked animation. useScroll and useTransform are solid for basic parallax and progress indicators, but if you need something like a 3D scene that scrubs frame-by-frame as the user scrolls, you're fighting the abstraction. In that case, reach for GSAP's ScrollTrigger or a Three.js-based solution.
That said, if you're building design-system-level components that other devs will consume — the kind of thing you'd find when you browse the components on a UI library — Framer Motion's declarative API makes it far easier for consumers to override and extend animations without reading your internal implementation.
Putting It Together: Animation in Real Products
The gap between "I understand Framer Motion" and "my animations feel polished" usually comes down to three things: timing, subtlety, and consistency.
On timing: most UI animations should live in the 150ms–400ms range. Anything under 100ms is imperceptible. Anything over 500ms makes users feel like the app is slow. The sweet spot for enter animations on cards and modals is around 250ms–320ms.
On subtlety: a 12px translate is almost always better than a 40px translate. You want the animation to register without distracting. If users are watching your animation instead of reading your content, it's too much. The glassmorphism components in Empire UI use 8px–16px offsets precisely because it's enough to feel intentional without being showy.
On consistency: pick a small set of animation primitives and reuse them. One spring config for interactive elements, one tween config for opacity fades, one set of variants for list entrance. Your users won't consciously notice consistency — but they'll notice when it's missing.
If you want to see these principles applied to specific UI styles — from the heavy physicality of neobrutalism to the ethereal floatiness of aurora — it's worth exploring how different design systems handle motion as a first-class design decision, not an afterthought.
Common Pitfalls and How to Dodge Them
The number one mistake is animating layout-affecting properties like width, height, or padding. These trigger layout recalculation on every frame and will tank performance, especially on mobile. Always prefer scale transforms over size changes, and use Framer Motion's layout prop when you genuinely need to animate layout shifts.
Second pitfall: forgetting to memoize variant objects. If you define your variants inline inside the component function, they get recreated on every render — which can cause unnecessary re-animations. Define them outside the component or memoize with useMemo.
Third: not testing on real devices. A 2026 MacBook Pro will make any animation look smooth. An older Android mid-range phone is where things fall apart. Keep your animation work lightweight, avoid heavy blur effects in motion (yes, backdrop-filter during animation is a trap), and always test before shipping.
One more thing — Framer Motion's LazyMotion feature lets you code-split the animation library so it doesn't bloat your initial bundle. If you're not using animations above the fold, LazyMotion with domAnimation or domMax feature sets is an easy win for load performance.
FAQ
Yes, but motion components are client components — add 'use client' to any file that uses them. Wrap your layout-level animations in a client boundary component to keep server components working correctly.
animate runs when the component mounts and reflects your controlled state. whileHover is a gesture prop that overrides styles while the cursor is over the element — it auto-resets when the cursor leaves, so you don't manage that state yourself.
Absolutely. Use Tailwind classes on className as usual, and pass animation values through Framer Motion's props. They don't conflict — Tailwind handles static styles, Framer Motion handles animated ones.
It's fine if you stick to transform and opacity animations. Avoid animating blur, box-shadow, or layout properties during animation on mobile — those are the real culprits, not the library itself.