Framer Motion Layout Animations: How the 'layout' Prop Handles DOM Transitions
Framer Motion's layout prop uses FLIP animations to smoothly transition elements when their bounding box changes. This guide covers the mechanism, worked examples for size toggles, shared-element transitions, and list reordering, plus performance limits and debugging tips.
05 Apr 2026, 11:54 UTC

The Problem: Jarring Layout Shifts When Elements Move
When React state changes cause elements to change size, position, or order in the DOM, browsers instantly recalculate layout. The result is a jarring jump—no transition, no visual continuity. Users lose spatial context, especially in lists, grids, or shared-element transitions between views.
Framer Motion's layout prop solves this by automatically animating an element from its previous bounding box to its new one using a FLIP (First, Last, Invert, Play) technique. You add the prop; the library handles the math.
How the layout Prop Works
When a motion component mounts with layout, Framer Motion records its initial bounding box (First). After React renders the next frame and the element settles at its new size or position (Last), the library computes the transform delta—translation and scale—and applies an inverted transform (Invert) so the element appears visually unchanged. It then animates that transform back to identity (Play) using a spring or tween.
Because the animation runs on transform and opacity only, it stays on the compositor thread and avoids layout thrashing. The element's actual CSS width, height, top, left remain untouched.
Worked Example: Toggling a Card's Size
This minimal example shows a card that expands on click. The layout prop animates the size change without any manual keyframes.
import { motion, useMotionValue, useSpring } from 'framer-motion';
import { useState } from 'react';
export function ExpandingCard() {
const [expanded, setExpanded] = useState(false);
return (
setExpanded(!expanded)}
style={{
width: expanded ? 400 : 200,
height: expanded ? 300 : 150,
background: '#2563eb',
borderRadius: 12,
cursor: 'pointer',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'system-ui',
}}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
{expanded ? 'Expanded' : 'Click to expand'}
);
}
Run this in any React 18+ project with framer-motion@11 installed. Click the card; it grows smoothly. Inspect the element in DevTools during animation—you'll see transform: matrix3d(...) updating each frame, not width/height.
Shared Element Transitions with layoutId
When two different components share a layoutId, Framer Motion treats them as the same visual element. As one unmounts and the other mounts (typically inside AnimatePresence), the library morphs the first into the second's position and size.
import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';
function Thumbnail({ src, onClick }) {
return (
);
}
function Fullscreen({ src, onClose }) {
return (
);
}
export function Gallery() {
const [selected, setSelected] = useState(null);
const images = ['/img1.jpg', '/img2.jpg', '/img3.jpg'];
return (
<>
{images.map((src) => (
setSelected(src)} />
))}
{selected && setSelected(null)} />}
);
}
The thumbnail expands to fullscreen, then collapses back to its grid position when closed. AnimatePresence mode="wait" ensures the exit animation completes before the new component mounts.
Combining with AnimatePresence for List Reordering
When siblings are added, removed, or reordered, their bounding boxes shift. Wrapping the list in AnimatePresence and giving each item layout makes those shifts animate.
import { motion, AnimatePresence, reorder } from 'framer-motion';
import { useState } from 'react';
export function ReorderableList() {
const [items, setItems] = useState(['A', 'B', 'C', 'D', 'E']);
function handleDragEnd({ oldIndex, newIndex }) {
setItems(reorder(items, oldIndex, newIndex));
}
return (
{items.map((item, index) => (
{item}
))}
);
}
Drag an item; siblings slide into their new positions. The reorder utility updates the array immutably.
Performance Limits and Common Mistakes
1. Large Lists Degrade Quickly
Each layout element measures its bounding box on every render. A list of 200+ items can drop frames during reorder. Mitigation: virtualize the list (e.g., react-window) and only apply layout to visible items, or use layout={false} on static items.
2. Absolute Positioning and Existing Transforms
If an element already has position: absolute or a CSS transform, the FLIP calculation can produce a visual jump on first render. Fix: wrap the animated element in a motion.div layout wrapper with position: relative, or ensure the element starts with transform: none.
3. Flex/Grid Containers Without Defined Size
When a flex or grid child animates layout, the container must have a stable size during the transition. If the container shrinks to fit content mid-animation, the child's target bounding box moves, causing a wobble. Give the container explicit width/height or min-height.
4. Nested layout Elements
Nested components with layout can compound transforms unexpectedly. Prefer a single layout on the outermost moving element; use layoutGroup on the parent if you need coordinated transitions across children.
Verifying the Animation Path
Open Chrome DevTools → Elements tab → select the animated element. In the Styles pane, watch the transform property during the transition. You should see a matrix3d() or translate3d() scale() value interpolating. If you see width, height, top, or left changing, something else (CSS transition, layout thrashing) is interfering.
For a programmatic check, add a onLayoutAnimationComplete callback to log the final bounding box:
console.log('done')}>…
When Not to Use layout
- Simple opacity/fade transitions—use
animate={{ opacity }}instead. - Elements that change size continuously (e.g., resizable panels with drag)—the per-frame measurement overhead adds up.
- SVG elements—
layoutworks on HTML boxes; for SVG useanimatewith explicitx/y/width/height.
Summary Checklist
- Add
layoutto anymotioncomponent whose bounding box changes. - Use
layoutId+AnimatePresencefor shared-element transitions between routes or modals. - Wrap lists in
AnimatePresencefor add/remove/reorder animations. - Keep animated lists under ~100 items; virtualize beyond that.
- Ensure containers have stable dimensions during transitions.
- Verify in DevTools that only
transformanimates.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.