Well-crafted motion turns static UIs into intuitive, responsive experiences. In React, two heavy-hitters lead the way: Framer Motion and React Spring. This guide shows when to use which, how to build common patterns (enter/exit, lists, dialogs, shared element transitions), and how to ship animations that are accessible, performant, and testable.
Related reading (internal):
Table of Contents
- When to use Framer Motion vs React Spring
- Setup with pnpm
- Design principles for motion
- Shared best practices (for both libraries)
- Framer Motion recipes
- React Spring recipes
- Gestures & drag (Framer Motion) and use-gesture (React Spring)
- Scroll-based effects & parallax
- Layout & shared element transitions
- Next.js & SSR considerations
- Accessibility: reduced motion & focus
- Testing animations (unit, e2e, visual)
- Troubleshooting & performance tuning
- FAQ
When to use Framer Motion vs React Spring
Framer Motion
- Best for: UI transitions, page/route animations, micro-interactions, layout animations, shared element transitions.
- Model: Declarative variants + spring/transition props.
- Strengths:
AnimatePresence,layout,layoutId,useScroll/useTransform, easy orchestration/stagger.
React Spring
- Best for: Physics-driven sequences, complex choreography, canvas/SVG/3D scenes, custom gesture physics.
- Model: Hook-based springs (
useSpring,useTransition,useSprings,useTrail). - Strengths: Detailed control over mass/tension/friction, interpolation chains, works great with @use-gesture/react.
Rule of thumb: if you need UI primitives to “just animate well”, start with Framer Motion. If you need fine-grained physics and full control, go with React Spring.
Setup with pnpm
# Framer Motion
pnpm add framer-motion
# React Spring (and optional gesture helpers)
pnpm add @react-spring/web @use-gesture/reactDesign principles for motion
- Purposeful, not decorative: Motion should clarify state changes, spatial relationships, or feedback.
- Use the right duration: 150–250ms for micro UI changes; 250–400ms for large context switches; longer for intros only.
- Ease & physics: Springs feel natural; easing curves suit simple fades/slides.
- Prefer transform/opacity: Avoid animating layout-affecting properties (top/left/width/height) unless using FLIP or
layout. - Respect accessibility: Honor
prefers-reduced-motionand keep focus rings visible during transitions.
Shared best practices (for both libraries)
- Animate presence: For mount/unmount transitions, wrap lists and conditionals with a provider (
AnimatePresencein FM,useTransitionin Spring). - Stagger responsibly: Small staggering (40–80ms) guides the eye without feeling slow.
- Keep state local: Animation controls near the animated node reduce re-renders.
- Avoid layout thrash: Batch reads/writes; prefer transforms; minimize expensive shadows/filters.
- Limit simultaneous motion: Competing elements create noise; animate one primary focus per surface.
Framer Motion recipes
Enter/Exit with AnimatePresence (drawer)
import { motion, AnimatePresence } from "framer-motion";
export function Drawer({
open,
onClose,
children,
}: {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
return (
<AnimatePresence>
{open && (
<motion.aside
initial={{ x: "100%", opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: "100%", opacity: 0 }}
transition={{ type: "spring", stiffness: 380, damping: 32 }}
className="fixed inset-y-0 right-0 w-[360px] bg-white shadow-xl"
role="dialog"
aria-modal="true"
>
<button
className="absolute right-3 top-3"
onClick={onClose}
aria-label="Close panel"
>
✕
</button>
{children}
</motion.aside>
)}
</AnimatePresence>
);
}Variants and stagger (list reveal)
import { motion } from "framer-motion";
const list = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.06, delayChildren: 0.04 },
},
};
const item = {
hidden: { y: 8, opacity: 0 },
show: { y: 0, opacity: 1 },
};
export function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul
variants={list}
initial="hidden"
animate="show"
className="space-y-2"
>
{items.map((text) => (
<motion.li key={text} variants={item} className="rounded-md border p-3">
{text}
</motion.li>
))}
</motion.ul>
);
}Layout animation (auto-measure + FLIP)
import { motion } from "framer-motion";
export function Expander({ open }: { open: boolean }) {
return (
<motion.div layout className="rounded-md border p-4">
<motion.h3 layout className="font-semibold">
Details
</motion.h3>
{open && (
<motion.p layout className="mt-2 text-sm text-neutral-600">
Auto-layout animations use FLIP under the hood when you pass `layout`.
</motion.p>
)}
</motion.div>
);
}Scroll-based animation (useScroll + useTransform)
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
export function ScaleOnScroll() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
const scale = useTransform(scrollYProgress, [0, 1], [0.95, 1.05]);
const opacity = useTransform(scrollYProgress, [0, 1], [0.7, 1]);
return (
<motion.div
ref={ref}
style={{ scale, opacity }}
className="h-64 rounded-xl bg-neutral-100"
/>
);
}React Spring recipes
Basic spring (useSpring)
import { useSpring, animated } from "@react-spring/web";
export function FadeUp({ children }: { children: React.ReactNode }) {
const styles = useSpring({
from: { opacity: 0, y: 24 },
to: { opacity: 1, y: 0 },
config: { tension: 280, friction: 24 },
});
return (
<animated.div
style={{
opacity: styles.opacity,
transform: styles.y.to((v) => `translateY(${v}px)`),
}}
>
{children}
</animated.div>
);
}List mount/unmount (useTransition)
import { useTransition, animated } from "@react-spring/web";
export function TodoList({
items,
}: {
items: { id: string; title: string }[];
}) {
const transitions = useTransition(items, {
keys: (item) => item.id,
from: { opacity: 0, transform: "translateY(8px)" },
enter: { opacity: 1, transform: "translateY(0)" },
leave: { opacity: 0, transform: "translateY(-8px)" },
});
return (
<ul className="space-y-2">
{transitions((style, item) => (
<animated.li
key={item.id}
style={style}
className="rounded-md border p-3"
>
{item.title}
</animated.li>
))}
</ul>
);
}Coordinated gestures (with @use-gesture/react)
import { useSpring, animated } from "@react-spring/web";
import { useGesture } from "@use-gesture/react";
export function DraggableCard() {
const [{ x, y, scale }, api] = useSpring(() => ({ x: 0, y: 0, scale: 1 }));
const bind = useGesture({
onDrag: ({ offset: [dx, dy] }) => api.start({ x: dx, y: dy }),
onPinch: ({ offset: [d] }) => api.start({ scale: 1 + d / 200 }),
});
return (
<animated.div
{...bind()}
style={{ x, y, scale }}
className="h-32 w-48 rounded-xl bg-indigo-500"
/>
);
}Gestures & drag (Framer Motion) and use-gesture (React Spring)
Framer Motion has built-in drag, momentum, constraints, and dragElastic—ideal for drawers, sliders, and cards.
React Spring pairs with @use-gesture/react for unified drag/pinch/wheel handlers that feed spring updates—perfect for bespoke physics.
Tip: Clamp draggable ranges; add snap points; disable drag for keyboard-only users or provide alternative controls.
// Framer Motion draggable
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: 0, bottom: 0 }}
dragElastic={0.12}
/>Scroll-based effects & parallax
- Framer Motion:
useScroll+useTransformsimplifies parallax and progress-linked UI. - React Spring: wire scroll position to
useSpringvalues and interpolate style outputs.
Avoid heavy parallax on content essential for comprehension; offer reduced-motion respect.
Layout & shared element transitions
Framer Motion:
layoutfor auto-resizing & FLIP.layoutIdto animate shared elements between routes or states (e.g., card → modal).AnimatePresencewithmode="wait"to ensure exit completes before enter.
React Spring:
- Simulate shared transitions with
useTransitionkeyed by element identity; animate transforms/opacity between states.
Keep aspect ratios and origin points consistent for believable morphs.
Next.js & SSR considerations
- Hydration safety: Trigger client-only animations in
useEffector guard withuseMountedflags to avoid server/client mismatch. - Route transitions: Use a layout wrapper with Framer Motion
AnimatePresenceto animate between pages. - Server Components: Confine animated pieces to Client Components; pass only serializable props.
- Image & LCP: Animate after critical content has rendered; don’t delay LCP.
Accessibility: reduced motion & focus
Respect user preferences and keep focus visible during transitions.
// Framer Motion: reduce motion
import { useReducedMotion } from "framer-motion";
const shouldReduce = useReducedMotion();
const transition = shouldReduce
? { duration: 0 }
: { type: "spring", stiffness: 400, damping: 30 };/* CSS fallback */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition-duration: 0.01ms !important;
}
}- Maintain focus order; avoid moving focus targets mid-transition.
- Provide non-animated equivalents of critical interactions.
Testing animations (unit, e2e, visual)
- Unit: mock time and assert final styles or class toggles.
- E2E (Playwright/Cypress): set
prefers-reduced-motion: reduce, wait for stable states, and assert visibility/placement. - Visual regression: Storybook captures across themes and motion preferences.
// Example: Playwright test setting reduced motion
await page.emulateMedia({ reducedMotion: "reduce" });Troubleshooting & performance tuning
- Jank on scroll: Avoid animating expensive properties; debounce scroll handlers; prefer
useScroll. - Layout shifts: Use
layout(FM) or FLIP techniques; reserve space with aspect-ratio or fixed heights. - Too bouncy/slow: Tweak spring stiffness/damping (FM) or tension/friction/mass (Spring).
- Conflicting transitions: Sequence animations; avoid simultaneous transforms on parent/child unless intentional.
- Large bundles: Import from
framer-motionor@react-spring/webonly; lazy-load heavy animated routes.
FAQ
Is Framer Motion overkill for simple fades?
No, but for very basic effects, CSS transitions may suffice—save libraries for orchestrated or gesture/scroll-linked animations.
Can I mix Framer Motion and React Spring?
Yes, at route or feature boundaries. Avoid animating the same element with both to prevent conflicts.
How do I support reduced motion?
Check prefers-reduced-motion, provide faster/no-op transitions, and keep focus/ARIA feedback intact.
What about performance on low-end devices?
Prefer transforms/opacity, limit blur/shadows/filters, and reduce simultaneous animations.
