Stop Animating Width: Framer Motion's layout Prop and the FLIP Trick That Does the Math for You
Framer Motion's layout prop uses FLIP to turn real CSS layout changes into cheap transform animations. Here's how it works, a working example, and where it breaks.
19 Aug 2025, 04:27 UTC

You have a settings panel that toggles between compact and expanded. A designer wants the card to glide open, not snap. Your first instinct is to animate width and height — and that's exactly where the trouble starts, because animating layout properties forces the browser to recalculate layout on every frame. Framer Motion's layout prop exists to solve this specific problem: it lets you change real CSS layout while animating with cheap GPU-friendly transforms under the hood.
The thesis is simple: let your layout be the source of truth, and let Framer Motion fake the movement.
What layout actually does (FLIP, briefly)
When you add layout to a motion.div, Framer Motion performs a FLIP animation — First, Last, Invert, Play. On each render where the element's bounding box changes, it measures the old position and size (First), measures the new ones after React commits the DOM change (Last), applies an inverse transform so the element visually stays put (Invert), then animates that transform to zero (Play). The user sees a smooth glide; the browser only ever composites transforms, never re-lays-out per frame.
The practical consequence: you write ordinary CSS — flexbox reordering, conditional padding, a grid column change — and the animation falls out for free. No coordinate math, no refs, no getBoundingClientRect in your own code.
A worked example: the expanding card
This assumes Framer Motion 10+ (the API is stable across recent major versions, but check your installed version with npm list framer-motion before copying). The component toggles between two sizes using a class change, not a style animation:
import { motion } from "framer-motion";
import { useState } from "react";
export function ExpandingCard() {
const [expanded, setExpanded] = useState(false);
return (
<motion.div
layout
onClick={() => setExpanded((e) => !e)}
style={{
width: expanded ? 400 : 200,
height: expanded ? 240 : 120,
borderRadius: 12,
background: "#6366f1",
}}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>
);
}Click it and the card glides between sizes. Inspect it in DevTools during the animation (Elements panel → Computed → filter for "transform") and you'll see transform: scale(...) translate(...) changing per frame while width and height sit at their final values. That inspection step is worth doing once — it's the fastest way to confirm FLIP is actually happening and you haven't accidentally fallen back to layout animation.
The same prop handles DOM reordering. In a list sorted by state, each item gets layout and a stable key; when the sort order changes, items slide to their new slots. Pair it with AnimatePresence for items entering and leaving, and you have a fully animated sortable list in roughly fifteen lines.
layoutId: the shared element illusion
A related but distinct feature: give two different components the same layoutId (say, a thumbnail in a grid and a hero image in a detail view), and Framer Motion animates between them when one unmounts and the other mounts. It's not one element moving — it's a crossfade-and-morph between two separate DOM nodes. This is the trick behind most "app store card opens into full screen" demos, and it works well as long as only one element with a given layoutId is mounted at a time.
Where it bites you
Three limitations matter in practice:
- Aspect-ratio stretching. FLIP uses
scaleX/scaleY, so if the element's aspect ratio changes dramatically, children (especially text and images) visibly distort mid-animation. Mitigation: putlayouton the child too so it counter-animates, or restructure so the ratio stays closer to constant. - Cost scales with element count. Every
layoutelement measures its bounding box on every relevant render. A few dozen is fine; a few hundred animating simultaneously in a large list can drop frames on mid-range devices. Profile with the Performance tab before shipping layout animations on big collections. - Unstable parents break measurements. If the parent container's own layout is shifting for unrelated reasons (late-loading fonts, images without dimensions), the measured "Last" position can be wrong and elements jump. Give media explicit dimensions and keep the surrounding layout stable.
Also note: layout composes cleanly with animate. You can change background color or opacity via animate while position changes via layout — they run on independent tracks and don't fight.
Try it, then verify it
Start with the expanding card above — it's the smallest case that proves the mechanism. Then open DevTools, record a performance profile while toggling, and confirm two things: no purple "Layout" bars dominating each frame, and transforms doing the visible work. If both hold, you've replaced a class of hand-rolled animation code with one prop, and your CSS stays honest. If frames drop, reduce the number of simultaneously animated elements before reaching for anything more exotic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.