Motion for React (Framer Motion) in 2026: layout, AnimatePresence, Gestures
Master Motion for React in 2026 — layout animations, AnimatePresence exit transitions, and gesture-driven UIs with practical code you can ship today.
What Changed in 2026 (and Why You Should Care)
The library you probably still call "Framer Motion" is now officially Motion for React — same Matt Perry, same API you know, just rebranded as the project matured into a standalone open-source effort decoupled from Framer the design tool. The npm package name changed to motion back in late 2024, but the mental model is identical. If your package.json still reads framer-motion: ^11, you're fine for now. The new canonical import is from 'motion/react'.
Motion v12, which shipped in early 2026, made a few things worth calling out: the layout prop got smarter about shared-element transitions between routes, useAnimate is now the recommended imperative API (replacing the older useAnimation), and the bundle size when tree-shaken properly dropped to around 18 kB gzipped. That last one matters if you've been deferring adoption because of bundle concerns.
In practice, the biggest day-to-day change is that motion components no longer require a separate domAnimation or domMax import to get gesture support — it's all included by default with no extra config. Less footprint ceremony, which I'm genuinely happy about.
Quick aside: if you're still on v10 because a major version bump seemed scary, the migration is literally two steps: update the package name and replace import { motion } from 'framer-motion' with import { motion } from 'motion/react'. That's it for 90% of projects.
The layout Prop: Automatic DOM Diffing Magic
Add layout to any motion.* element and Motion will interpolate its position and size whenever those change between renders. No manual getBoundingClientRect calls, no transform math, no requestAnimationFrame loops. It's honestly one of the most underrated features in any animation library — you'd spend a weekend writing what layout gives you for free.
import { motion } from 'motion/react';
function ExpandableCard({ isExpanded }: { isExpanded: boolean }) {
return (
<motion.div
layout
style={{
width: isExpanded ? 480 : 240,
padding: isExpanded ? 32 : 16,
borderRadius: 12,
background: 'rgba(255,255,255,0.08)',
}}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<motion.h2 layout="position">Card Title</motion.h2>
{isExpanded && <p>Extra content revealed on expand.</p>}
</motion.div>
);
}One subtle thing: layout="position" on child elements tells Motion to animate only the position, not the size. Without this, text nodes inside a resizing parent will squish during the transition — not what you want. Worth noting: you can also use layout="size" if you only care about size changes and don't want position interpolation.
For list reordering — think drag-and-drop or sorted search results — you wrap items in <LayoutGroup> and each item gets layout. Motion then synchronizes the animations across all siblings so items don't teleport around when indexes shift. This pattern is what every Kanban board on the internet should be using but isn't.
import { motion, LayoutGroup } from 'motion/react';
function SortableList({ items }: { items: string[] }) {
return (
<LayoutGroup>
{items.map((item) => (
<motion.li layout key={item} transition={{ duration: 0.25 }}>
{item}
</motion.li>
))}
</LayoutGroup>
);
}AnimatePresence: Proper Exit Animations Without Tears
React unmounts components immediately. That's fine for logic, terrible for animation — there's zero time to play an exit sequence if the element is already gone from the DOM. AnimatePresence solves this by keeping the component mounted until its exit animation finishes, then removing it cleanly.
import { motion, AnimatePresence } from 'motion/react';
function Modal({ isOpen, children }: { isOpen: boolean; children: React.ReactNode }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.95, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 8 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
style={{ position: 'fixed', inset: 0, display: 'grid', placeItems: 'center' }}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}The key prop on the animated child is not optional here — Motion uses it to track identity across renders. Forget it and your exits won't fire. That's the number-one bug people hit with AnimatePresence.
Where it gets more interesting: the mode prop. Default is 'sync' (enter and exit happen simultaneously). mode="wait" makes the new component wait until the old one finishes exiting — great for page transitions. mode="popLayout" is the newest addition; it pops the exiting element out of document flow instantly so layout doesn't shift, then animates the exit on top. Pair this with layout for list removal and you get silky-smooth item deletions without content jumping.
Honestly, the page-transition use case is where AnimatePresence really shines. Wrap your Next.js route outlet with it, give each page a consistent key, and you get proper crossfade or slide transitions without any third-party router integration. Check out the page-transitions-nextjs article here on Empire UI if you want the full Next.js App Router wiring.
Gestures: Drag, Hover, and Tap Without the Boilerplate
Motion gives you gesture props directly on motion.* elements — whileHover, whileTap, whileDrag, and whileFocus. Each one accepts the same variant-style object as animate. Hover state with CSS would be fine for color changes, but try doing a spring-physics scale with CSS and you immediately run into transition limitations. Motion's gesture props are where CSS genuinely can't compete.
import { motion } from 'motion/react';
function PressButton({ children }: { children: React.ReactNode }) {
return (
<motion.button
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.96 }}
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
style={{ padding: '12px 24px', borderRadius: 8 }}
>
{children}
</motion.button>
);
}Drag is where things get really fun. Add drag (or drag="x" for axis-constrained) and your element becomes draggable immediately. dragConstraints lets you pass a ref to a container element or a bounding box object in pixels. dragElastic controls how far past constraints the element can stretch — 0 means rigid, 1 means fully elastic. A value around 0.2 gives that satisfying iOS rubber-band feel.
import { motion, useRef } from 'react';
function DragCard() {
const constraintsRef = useRef(null);
return (
<div ref={constraintsRef} style={{ width: 400, height: 300, position: 'relative' }}>
<motion.div
drag
dragConstraints={constraintsRef}
dragElastic={0.15}
whileDrag={{ scale: 1.05, cursor: 'grabbing' }}
style={{
width: 120,
height: 120,
borderRadius: 16,
background: 'rgba(139,92,246,0.7)',
cursor: 'grab',
}}
/>
</div>
);
}One more thing — useDragControls lets you start a drag from any element, not just the draggable one itself. Classic use case: a drag handle icon on the side of a card, while the rest of the card has normal pointer events. The useMotionValue and useTransform hooks pair with drag to build things like velocity-based fling animations and progress indicators that actually feel physical. Browse the gradient generator on Empire UI if you want to see smooth value-driven UI in action.
Motion Values and useTransform: Reactive Animation State
Motion values are the reactive primitive under the hood. When you use whileHover or animate, Motion is creating and updating motion values internally. You can create them yourself with useMotionValue and pipe them through useTransform to derive new values.
import { motion, useMotionValue, useTransform } from 'motion/react';
function TiltCard() {
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useTransform(y, [-150, 150], [15, -15]);
const rotateY = useTransform(x, [-150, 150], [-15, 15]);
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
const rect = e.currentTarget.getBoundingClientRect();
x.set(e.clientX - rect.left - rect.width / 2);
y.set(e.clientY - rect.top - rect.height / 2);
}
function handleMouseLeave() {
x.set(0);
y.set(0);
}
return (
<motion.div
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ rotateX, rotateY, perspective: 800 }}
>
Hover me
</motion.div>
);
}The key insight here: motion values don't trigger React re-renders. They bypass the React reconciler entirely and update the DOM directly via requestAnimationFrame. That's why smooth 60 fps (or 120 fps on ProMotion displays) animations are achievable even with complex transforms — you're not paying the React render cycle cost for every frame.
useTransform isn't limited to simple linear maps. The third argument can be an easing function, or you can pass non-linear input/output pairs — useTransform(scrollY, [0, 200, 400], [0, 1, 0]) for a fade-in-then-fade-out as the user scrolls, for instance. And useSpring wraps any motion value in spring physics, so you can make scroll-linked animations feel weighted rather than mechanical.
Look, if you're building anything beyond simple hover effects — parallax, scroll-driven reveals, 3D tilt cards, cursor-follower effects — motion values are the API you want to reach for. The declarative animate prop is great for entry states, but useMotionValue + useTransform is where expressive, physics-based UI actually lives. You can pair this with custom cursors from Empire UI to create the full interactive feel.
Variants: Orchestrating Multi-Element Animations
Variants let you define named animation states as objects and reference them by name on the animate prop. The payoff isn't just cleaner code — it's orchestration. A parent motion element with staggerChildren in its variant transition will automatically delay each child's animation sequentially, no manual delay math required.
import { motion } from 'motion/react';
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3 } },
};
function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.li key={item} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}The cascade works because variant names propagate down the tree automatically. You don't pass initial or animate to the children at all — they inherit from the parent. This is the right way to build any list, card grid, or nav menu that should animate in item by item.
That said, be careful about deeply nested variant trees. If a child has its own local animate prop, it overrides the cascaded variant. This isn't a bug, but it will bite you when you're trying to debug why one card in a grid isn't staggering. Always check whether a child element has an explicit animate before assuming the parent orchestration should handle it.
If you're building styled components with glassmorphism components or any of the Empire UI style hubs, variants are the glue that makes multi-step reveal animations feel polished without a giant pile of timeline code. Define three or four named states, wire them to your component logic, and let Motion handle the interpolation.
Performance Tips You Actually Need
The main rule: animate transform properties (x, y, scale, rotate, skew) and opacity. These are GPU-composited — the browser doesn't need to recalculate layout or paint on every frame. Animating width, height, top, left, margin, or padding triggers layout, which tanks performance, especially on lower-end Android devices that have been around since around 2022 and are still widely used.
Motion's layout prop is an exception to this — it does animate width and height internally, but it does so by calculating the delta and applying it via transform: scale() plus a counter-scale on children to prevent distortion. You get the visual effect of dimension changes without the layout thrash. It's a well-known trick called FLIP (First, Last, Invert, Play).
// Bad — animates width directly, triggers layout
<motion.div animate={{ width: isOpen ? 320 : 80 }} />
// Better — use layout prop instead
<motion.div
layout
style={{ width: isOpen ? 320 : 80 }}
transition={{ type: 'spring', stiffness: 280, damping: 28 }}
/>Use the will-change: transform CSS property sparingly and only on elements you know will animate — it tells the browser to promote the element to its own compositor layer ahead of time. Slapping it on everything kills memory. Motion adds it automatically during active animations and removes it after, which is the right behavior. Don't override that.
Finally, useReducedMotion() deserves a mention. It returns true if the user has prefers-reduced-motion enabled. Wire it up at your animation decision points and skip or reduce animations accordingly. Accessibility isn't optional, and in 2026 there's really no excuse for ignoring it. Check wcag-accessibility-guide for the broader picture, but for Motion specifically: wrap your variant definitions in a conditional based on useReducedMotion() and you're covered.
FAQ
They're the same library. The npm package was renamed from framer-motion to motion and the React-specific import is now from 'motion/react'. The API didn't change — just the package name and import path.
Yes to both. Wrap your route outlet with AnimatePresence mode="wait" and give each page component a stable key tied to the route path. The App Router's layout system means you only animate what's actually changing between routes.
Remove any CSS transition on properties that Motion's layout prop controls — mostly width, height, and transform. Two animation systems fighting over the same property causes jank. Let Motion own the transform, and use CSS only for color, opacity on non-motion elements.
Yes. With proper named imports and a modern bundler, you can get the core down to roughly 18 kB gzipped. If you only use motion.div with basic props, you won't pull in the drag or scroll APIs unless you import them.