TL;DR Use CSS for micro-interactions between two fixed states, like hover, press and toggles; it costs no JavaScript. Use Motion (formerly Framer Motion) for exit animations, layout changes, interruptible springs and gestures. In my Next.js test the Motion route shipped 69 kB more first-load JS than the CSS one.
Framer Motion vs CSS animation is mostly a question of what the animation has to do. Use CSS transitions for micro-interactions that move between two fixed states: hover, focus, a button press, a toggle knob, a color change. Reach for Motion (the library formerly called Framer Motion) when you need what CSS can't do cleanly: animating an element as it leaves the DOM, animating layout changes such as reordering or a highlight moving between items, physics springs that can be interrupted mid-flight, and drag or gesture input. In my test app, a route using Motion for a toggle and a dismissible list loaded 172 kB of first-load JS, against 103 kB for the same UI built with CSS.
Below I build the same toggle and the same press feedback both ways in a Next.js 15 app, with reduced motion handled in each. Most comparisons stop at opinions or quote somebody else's bundle numbers. These numbers come from my own build output, and there's a section on the newer CSS features that cover part of what used to need a library.
Yes. Framer Motion was renamed to Motion and is published as the motion package. The React API is imported from motion/react, and the upgrade guide covers the switch. The framer-motion package still exists, so you'll see both in the wild. Every example here uses:
bash
npm i motion
Hold to Delete running live. A delete button you press and hold, then the file crumples into the bin.
The styles go in a CSS module. The knob moves with transform, which the browser can animate without recalculating layout, and the cubic-bezier with a value above 1 gives a small overshoot that feels a bit like a spring:
The press feedback on the Save button is the .press class: scale down to 96% while :active. It needs no JavaScript at all, so it works inside a Server Component:
tsx
import styles from "./toggle.module.css";import { CssToggle } from "./CssToggle";export default function Page() { return ( <main className="flex items-center gap-8 p-10"> <CssToggle label="Email alerts" /> <button type="button" className={`${styles.press} rounded-md bg-neutral-900 px-4 py-2 text-white`}> Save </button> </main> );}
Reduced motion is the @media (prefers-reduced-motion: reduce) block at the bottom of the stylesheet, which removes the transitions so the toggle switches instantly (prefers-reduced-motion on MDN).
What CSS does well here:
Zero extra JavaScript for the animation itself.
Transitions on transform and opacity run on the compositor in modern browsers.
Transitions are interruptible. If you click the toggle twice quickly, the knob reverses from where it is.
Same toggle, built with Motion. Instead of moving the knob with a transform, the track flips its justify-content, and layout tells Motion to animate the knob from its old position to its new one with a spring:
A spring has no fixed duration. It runs until it settles, and if you click mid-animation it carries its current velocity into the new direction. For a toggle, that's a nice detail but not a necessity. The CSS version looks almost the same.
Where Motion really earns its place is this second component. Each click on Save adds a "Saved" item, and each item can be dismissed:
Exit animations. When an item is removed from state, React removes its DOM node right away. AnimatePresence keeps it mounted until its exit animation finishes (AnimatePresence docs). With CSS you'd have to delay the state update yourself and keep the timings in sync.
Layout animations. When an item leaves, the ones below it slide up instead of jumping, because each li has layout. Doing that in CSS means measuring positions and animating transforms by hand.
The Save button uses whileTap, Motion's version of :active. It's here because the component already loads Motion, not because CSS couldn't do it.
Reduced motion is handled once, at the page level:
tsx
import { MotionConfig } from "motion/react";import { MotionToggle } from "./MotionToggle";import { Toasts } from "./Toasts";export default function Page() { return ( <MotionConfig reducedMotion="user"> <main className="flex items-start gap-8 p-10"> <MotionToggle label="Email alerts" /> <Toasts /> </main> </MotionConfig> );}
reducedMotion="user" follows the OS setting. Per the MotionConfig docs, transform and layout animations are disabled when reduced motion is on, while opacity and color animations still run. So the toast still fades in, and nothing slides.
I built both routes in the same app with next build (Next.js 15.5, motion 14.4.0). The "First Load JS" column reported:
├ ○ /framer-motion-vs-css-micro-interactions/css 435 B 103 kB
├ ○ /framer-motion-vs-css-micro-interactions/motion 17.1 kB 172 kB
+ First Load JS shared by all 102 kB
So in this app, the Motion route loaded 69 kB more JavaScript before it could become interactive. That's the full motion component with layout, AnimatePresence and MotionConfig. Motion documents ways to load less, such as LazyMotion with the smaller m component, but I didn't measure them for this post, so I won't put a number on it.
Is 69 kB a lot? On a dashboard that already ships a charting library, probably not. On a marketing page whose only animation is a button hover, it's a lot of code for something CSS does for free.
Partly, and this is where the comparison has moved since most posts on it were written. Two CSS features close some of the gap:
@starting-style gives an element a "before" state, so it can transition in the moment it appears. MDN lists it as Baseline 2024, working in current Chrome, Safari and Firefox.
transition-behavior: allow-discrete (the allow-discrete keyword in the transition shorthand) lets display wait for the other transitions, so an element can fade out before it's hidden.
Together they cover the classic case of a popover or dialog that fades and slides in and out, with no JavaScript. This one uses the native popover attribute, so the page stays a Server Component:
tsx
import styles from "./popover.module.css";export default function Page() { return ( <main className="p-10"> <button type="button" popoverTarget="tips" className="rounded-md border px-4 py-2"> Show tips </button> <div id="tips" popover="auto" className={styles.panel}> Press Esc or click outside to close. </div> </main> );}
This passed tsc and next build in the same test app. Older browsers simply skip the animation and show or hide the panel instantly, which is a fine fallback for a micro-interaction.
What CSS still can't do here is the toast list above. Those items aren't toggled with display; React removes them from the tree, and no stylesheet can animate a node that's already gone. The neighbors sliding up to fill the gap is a layout animation, which CSS also doesn't do on its own. That's the line.
The element moves between two known states (hover, focus, pressed, on/off, open/closed with a fixed height).
The element exists both before and after the animation.
You want the component to stay a Server Component, or close to it.
Use Motion when:
Something needs to animate out as it's removed (toasts, modals, list items, route content).
Layout changes should animate: reordering, items appearing in a list, a highlight moving between tabs.
You want springs that respond to velocity, or drag, pan and other gestures.
Several elements need to coordinate (stagger, shared layoutId between components).
Mixing them is fine, and it's what I usually do. The toggles and buttons get CSS transitions, and Motion is loaded only on the routes that have a list, a drawer or a gesture. Since every Motion component needs "use client", keeping it in small leaf components also keeps the rest of the page on the server.
Most of what I build mixes the two: CSS for hover and press states, Motion for the parts that enter, leave or change layout. The micro-interactions collection shows that split in practice, the hold-to-delete button is a good example of a press interaction that needs more than :active, and a drawer like the cart drawer is the kind of component where enter and exit animation matters most. If you'd rather read the code first, /free gives you one component with its full source.
I make animated React components at qodesign, and choosing between CSS and Motion is the first decision on every one of them.
Yes, it was renamed. The library is now Motion, published as the motion package with the React API in motion/react. The framer-motion package still installs, so older tutorials keep working.
Yes. motion.dev is the home of Motion, the same library by the same author, renamed when it became independent from Framer. The React components and props carried over.
Not by itself. The animations run on transforms and opacity like good CSS does; the real cost is the JavaScript it adds. In my build that was 69 kB of first-load JS for one route, which matters most on light marketing pages.
They aim at different jobs. Motion fits React components with declarative props, layout and exit animations, while GSAP is a general timeline engine that shines on long, scripted sequences and scroll scenes. For micro-interactions in a React app I'd pick CSS first and Motion second.