Comparing React Animation Libraries: CSS, Framer Motion, and React Spring
Animations add life to user interfaces — they make transitions feel natural and interactions more intuitive.
In React, developers have three powerful options to handle motion: native CSS animations, Framer Motion, and React Spring.
This guide compares them in-depth, with code examples, performance metrics, and scenario-based recommendations.
🎨 CSS Animations
CSS animations are browser-native, lightweight, and ideal for simple, performant transitions.
Example: Fade Toggle
import React, { useState } from 'react';
import './fade.css';
export default function FadeInComponent() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(!visible)}>
{visible ? 'Hide' : 'Show'} Box
</button>
<div className={`fade-box ${visible ? 'visible' : ''}`}>
Smooth Fade Effect
</div>
</div>
);
}
/* fade.css */
.fade-box {
opacity: 0;
transform: translateY(10px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.fade-box.visible {
opacity: 1;
transform: translateY(0);
}Example: Slide-in Animation
@keyframes slideIn {
0% { transform: translateX(-100%); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
.slide-in {
animation: slideIn 0.5s ease-out forwards;
}Pros
- Zero dependencies, optimized by the browser
- Perfect for hover effects and micro-interactions
- Minimal bundle size impact
Cons
- Limited control from JavaScript
- Hard to coordinate with React state logic
- No physical or dynamic motion support
⚡ Framer Motion
Framer Motion provides a declarative, physics-aware animation system with extensive React integration.
Example: Fade In & Out
import { motion } from 'framer-motion';
import React, { useState } from 'react';
export default function MotionFade() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(!visible)}>Toggle Visibility</button>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: visible ? 1 : 0, y: visible ? 0 : 10 }}
transition={{ duration: 0.4 }}
style={{
width: 200,
height: 100,
background: '#4ade80',
borderRadius: 8,
marginTop: 16,
}}
/>
</div>
);
}Example: Draggable Box
import { motion } from 'framer-motion';
export function DraggableBox() {
return (
<motion.div
drag
dragConstraints={{ left: 0, right: 300, top: 0, bottom: 200 }}
whileTap={{ scale: 0.95 }}
style={{
width: 100,
height: 100,
background: '#3b82f6',
borderRadius: '12px',
cursor: 'grab',
}}
/>
);
}Example: Page Transition
import { motion, AnimatePresence } from 'framer-motion';
import { Routes, Route, useLocation } from 'react-router-dom';
export function AnimatedRoutes() {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
<Routes location={location}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</motion.div>
</AnimatePresence>
);
}Pros
- Intuitive component-based API
- Excellent for drag, scroll, and gesture-based animations
- Built-in layout and exit animations
Cons
- Slightly heavier (~14 KB gzipped)
- Requires conceptual understanding of variants
- Overkill for static transitions
🌿 React Spring
React Spring is a physics-based animation library focused on realism and fluid motion.
Example: Spring Button
import { useSpring, animated } from 'react-spring';
export default function SpringButton() {
const style = useSpring({
from: { transform: 'scale(0.9)', opacity: 0 },
to: { transform: 'scale(1)', opacity: 1 },
config: { tension: 180, friction: 18 },
});
return <animated.button style={style}>Springy Button</animated.button>;
}Example: Trail Animation
import { useTrail, animated } from 'react-spring';
export function TrailList() {
const items = ['A', 'B', 'C', 'D', 'E'];
const trail = useTrail(items.length, {
from: { opacity: 0, transform: 'translateY(20px)' },
to: { opacity: 1, transform: 'translateY(0px)' },
});
return (
<div>
{trail.map((style, i) => (
<animated.div key={i} style={style}>
{items[i]}
</animated.div>
))}
</div>
);
}Example: Scroll-based Motion
import { useScroll, animated } from 'react-spring';
export function ScrollFade() {
const { scrollYProgress } = useScroll();
return (
<animated.div
style={{
opacity: scrollYProgress,
transform: scrollYProgress.to(
(y) => `translateY(${y * 40}px)`
),
}}
>
Scroll-triggered Motion
</animated.div>
);
}Pros
- Natural physical animations (spring, damping, inertia)
- Lightweight (~8 KB gzipped)
- Works seamlessly with 3D (Three.js)
Cons
- Requires understanding of physics configs
- Fewer high-level helpers
- Smaller ecosystem than Framer Motion
⚖️ Performance & Use-Case Comparison
| Library | Size (gzipped) | Simple Animations | Complex Interactions |
|---|---|---|---|
| CSS Animations | 0 KB | ✅✅✅ | ❌ |
| Framer Motion | ~14 KB | ✅✅ | ✅✅✅ |
| React Spring | ~8 KB | ✅✅ | ✅✅✅ |
Best Use Cases
- CSS: Lightweight effects, static transitions
- Framer Motion: Interactive UI, routing transitions
- React Spring: Realistic, continuous, physics-based motion
🧩 Modal Animation Example
CSS Version
function Modal({ open, onClose, children }) {
if (!open) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
/* CSS */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s forwards;
}
.modal-content {
background: #fff;
padding: 24px;
border-radius: 10px;
animation: scaleIn 0.3s forwards;
}
@keyframes fadeIn { to { opacity: 1; } }
@keyframes scaleIn { to { transform: scale(1); } }Framer Motion Version
import { motion } from 'framer-motion';
export function MotionModal({ open, onClose, children }) {
if (!open) return null;
return (
<motion.div
className="modal-overlay"
onClick={onClose}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="modal-content"
onClick={(e) => e.stopPropagation()}
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
exit={{ scale: 0.8 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</motion.div>
);
}React Spring Version
import { useSpring, animated } from 'react-spring';
export function SpringModal({ open, onClose, children }) {
const style = useSpring({
opacity: open ? 1 : 0,
transform: open ? 'scale(1)' : 'scale(0.8)',
config: { duration: 300 },
});
if (!open) return null;
return (
<animated.div className="modal-overlay" onClick={onClose} style={style}>
<animated.div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
{children}
</animated.div>
</animated.div>
);
}🧠 Summary
Animation enhances usability and adds emotional value to interfaces.
The right library depends on your project size, interaction complexity, and performance goals.
- CSS Animation: Lightweight and fastest for basic effects.
- Framer Motion: Best all-rounder for rich, interactive experiences.
- React Spring: Ideal for smooth, physics-driven animations.
For most modern React apps, Framer Motion strikes the best balance between expressiveness and developer ergonomics.