Motion vs CSS Animations in React: Which to Use When
By Hizmedia · · 6 min read
CSS can animate a lot on its own, and the Motion library (formerly Framer Motion) can animate almost anything. Choosing well keeps your bundle small and your animations smooth. The short version: use CSS for simple state changes and loops, and reach for Motion when animation depends on JavaScript state, physics, gestures, scroll or elements entering and leaving.
What CSS does well
Transitions and keyframes are built into the browser, cost no JavaScript and run on the compositor when you animate transform and opacity. They are the right tool for:
- Hover and focus states: colour, shadow and a small lift.
- Loops that never change: a spinner, a shimmer, a marquee.
- Simple enter effects triggered by a class, such as a fade-in when a panel opens.
<button className="rounded-xl bg-teal-700 px-5 py-3 transition hover:-translate-y-0.5 hover:shadow-lg motion-reduce:transition-none">
Get started
</button>Where Motion earns its place
- Springs: movement with weight that responds to how fast something moves, which CSS can only fake with fixed curves.
- Exit animations:
AnimatePresencekeeps an element on screen long enough to animate out. CSS can't animate an element React has already removed. - Layout animation:
layoutandlayoutIdanimate an element smoothly to a new size or position, like a tab highlight sliding between tabs. - Gestures: drag, hover and tap with physics, including flinging a card away.
- Scroll:
useScrollandwhileInViewtie animation to scroll position or visibility. - Values that come from JavaScript: following the pointer, counting a number up, drawing a chart.
"use client";
import { motion } from "motion/react";
// the highlight glides between tabs because every tab's highlight shares one layoutId
{tabs.map((tab) => (
<button key={tab.id} onClick={() => setActive(tab.id)} className="relative px-4 py-2">
{active === tab.id && <motion.span layoutId="highlight" className="absolute inset-0 rounded-lg bg-teal-700" />}
<span className="relative">{tab.label}</span>
</button>
))}Performance: what actually matters
The library you choose matters less than what you animate. Animating transform and opacity is cheap in both; animating width, height, top or box-shadow makes the browser lay out or repaint the page every frame, in both.
Motion runs its animations through the browser's Web Animations API where it can, so many of them are hardware accelerated just like CSS. Its cost is mostly the JavaScript it adds, which is why it's worth loading only on the parts of a page that need it: in Next.js, keep pages as Server Components and mark only the animated pieces with "use client".
Using both together
Most polished interfaces use both: CSS for hover states and simple loops, Motion for the moments that need physics, exits or scroll. MotionKit components do the same, which is why many of them are a Motion component with Tailwind classes for everything that doesn't move.
Whichever you use, respect reduced motion: Tailwind's motion-reduce: variant for CSS, and Motion's useReducedMotion hook or MotionConfig reducedMotion="user" for Motion.