Difficulty: Intermediate
What are the different approaches to animations in React? What is Framer Motion?
Animations in React is one of those topics where there are genuinely many valid approaches, and the right choice depends on how complex your animations are, how much control you need, and what your performance requirements are. Let me walk you through the landscape.
At the simplest level, you have plain CSS transitions and animations. These are the workhorses of web animation and they should be your first choice for simple things like hover effects, fade-ins, color changes, and sliding elements. You toggle a CSS class or change a style property, and CSS handles the interpolation. The big advantage is that CSS transitions run on the browser's compositor thread -- they don't block JavaScript execution and they can hit 60fps even on slower devices. If you can solve your animation with CSS, you probably should.
The className toggle approach works well in React. You maintain a boolean state like isOpen, and conditionally apply a class: className={`panel ${isOpen ? 'panel-open' : 'panel-closed'}`}. Your CSS defines the transition and the target values for each class. Simple, performant, and doesn't require any library.
But CSS has a big limitation: it cannot animate elements being removed from the DOM. When a React component unmounts (conditionally rendered with isOpen && <Modal />), it just disappears instantly. The element is removed from the DOM before any exit animation can play. This is the fundamental problem that Framer Motion solves.
Framer Motion is the most popular animation library in the React ecosystem, and its killer feature is AnimatePresence. This component wraps conditionally rendered children and delays their actual removal from the DOM until exit animations complete. You define initial (starting state), animate (target state), and exit (unmounting state) on a motion.div, wrap the conditional render in AnimatePresence, and Framer Motion handles the rest. The element animates out gracefully before being removed.
The variants system in Framer Motion is another huge win. Instead of defining animation properties inline, you create named animation states as objects. A parent can orchestrate child animations using staggerChildren -- each child starts its animation with a delay relative to its siblings, creating cascading entrance effects. You define the variants once and reference them by name, which keeps your JSX clean.
Framer Motion also provides the layout prop, which is almost magical. When a component's size or position changes (items reorder in a list, an accordion expands, a sidebar collapses), you just add layout to the motion component and it automatically animates from the old position to the new one using the FLIP technique (First, Last, Invert, Play). Without this, you'd have to manually calculate position changes and animate them.
The spring physics system deserves mention too. Instead of duration-based easing (ease-in, ease-out), spring animations use damping and stiffness parameters that simulate real physics. They feel more natural because real objects don't move in cubic bezier curves -- they have momentum and settle into place. Spring animations also respond naturally to interruption: if you reverse an animation mid-way, it doesn't awkwardly restart.
For more complex scenarios, React Spring is another great option. It's purely physics-based and has great support for animating values that aren't CSS properties -- like SVG path data, scroll positions, or canvas elements. Its interpolation system is powerful for mapping one animated value to multiple outputs.
GSAP (GreenSock Animation Platform) is the heavy hitter for complex timeline-based animations. If you need precise choreography -- this element starts at 0.2 seconds, then that element starts at 0.4 seconds, then they both reverse together -- GSAP's timeline system is unmatched. It integrates with React through useEffect and refs, though it feels less 'React-native' than Framer Motion.
Performance-wise, the golden rule is: only animate transform and opacity. These two CSS properties are composited by the GPU without triggering layout recalculation or repainting. Animating width, height, top, left, margin, padding, or font-size triggers layout reflows that are expensive and janky. If you need to animate size, animate transform: scale() instead. If you need to animate position, animate transform: translate() instead.
One more thing that often gets overlooked: accessibility. Some users experience motion sickness or discomfort from animations. The prefers-reduced-motion media query lets you detect this preference and either reduce or eliminate animations for those users. Framer Motion has a useReducedMotion hook for this. In any production app, respecting this preference is not optional -- it's an accessibility requirement.
import { motion, AnimatePresence } from 'framer-motion';
// Basic animation
function FadeIn() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
Content
</motion.div>
);
}
// Variants for coordinated animations
const listVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1 // Each child delays 100ms
}
}
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 }
};
function AnimatedList({ items }) {
return (
<motion.ul
variants={listVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
);
}
Variants define named animation states. staggerChildren cascades animations through children automatically.
import { AnimatePresence, motion } from 'framer-motion';
// AnimatePresence enables exit animations when components unmount
function Modal({ isOpen, onClose }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
className="modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
>
<motion.div
className="modal"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
onClick={e => e.stopPropagation()}
>
Modal content
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
// Layout animations
function ExpandingCard({ isExpanded, onClick }) {
return (
<motion.div
layout // Automatically animates layout changes
onClick={onClick}
style={{ height: isExpanded ? 300 : 100 }}
>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Expanded content
</motion.p>
)}
</motion.div>
);
}
AnimatePresence keeps components mounted until exit animation completes. layout prop automatically animates height/width changes with FLIP technique.
Framer Motion, CSS Transitions, useSpring, AnimatePresence, Performance