Skip to main content

React Layout Animations: How to Animate Component

Layout animation detects when an element's position or size changes in the DOM and smoothly transitions between the old and new layout states. Rather than snapping to the new position, the element glides, resizes, and reflows with automatic interpolation—all without explicit keyframe code.

In Framer Motion, the layout prop on a motion component enables this behavior. When a parent reorders children, changes grid columns, or alters padding, child elements automatically animate to their new positions and sizes. This eliminates jank and reduces the component code from 40+ lines of manual animation to a single prop.

How Layout Animation Works

When a component with layout prop renders, Framer Motion captures its bounding box (position, width, height). On the next render, if the bounding box changes, Motion measures the delta and creates a smooth animation from the old to the new position. This happens at 60 FPS via GPU-accelerated transform properties, bypassing expensive DOM reflows.

The magic is measurement: before animating, Framer Motion reads the old and new layout values using getBoundingClientRect(), then uses CSS transform to move the element visually without triggering reflow. This keeps animations fast and the main thread unblocked.

Layout Animation vs. CSS Transitions

CSS transition: all 300ms is tempting but slow. It animates every property—width, height, top, left—each of which triggers a reflow. Framer Motion animates only transform and opacity, keeping reflow count constant regardless of layout complexity. For a list of 20 items reordering, CSS transition causes 20 reflows per frame; Framer Motion causes 1.

Basic Layout Animation Example

Here is a button grid where clicking a button reorders the list. The buttons animate to their new positions automatically:

import { motion } from 'framer-motion';
import { useState } from 'react';

export default function ReorderingGrid() {
const [items, setItems] = useState(['A', 'B', 'C', 'D']);

const shuffle = () => {
const newItems = [...items];
const randomIndex = Math.floor(Math.random() * newItems.length);
[newItems[0], newItems[randomIndex]] = [newItems[randomIndex], newItems[0]];
setItems(newItems);
};

return (
<div style={{ padding: '20px' }}>
<button onClick={shuffle}>Shuffle</button>

<motion.div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '10px',
marginTop: '20px',
maxWidth: '200px',
}}
>
{items.map((item) => (
<motion.button
key={item}
layout
style={{
padding: '20px',
fontSize: '16px',
backgroundColor: '#0066cc',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
}}
>
{item}
</motion.button>
))}
</motion.div>
</div>
);
}

When you click "Shuffle," the array reorders, and each button animates to its new grid position over 250 ms (Framer Motion's default). No keyframes, no duration prop—just layout. This is the power of declarative layout animation.

Flexible vs. Group Layout

Two layout modes exist:

  • layout: Child elements animate independently to their new positions (default).
  • layout="group": Parent animates as a container; children animate relative to parent motion.

Use layout for grids and lists (each item moves independently). Use layout="group" for modal/sidebar scenarios where children should follow the parent's motion path.

<motion.div layout="group" style={{ width: isOpen ? '300px' : '50px' }}>
{/* Children animate relative to this container */}
<motion.div layout>{children}</motion.div>
</motion.div>

Layout with Size Changes

When a component's width or height changes (e.g., collapsing a sidebar or expanding a card), layout animates the resize smoothly. Combined with overflow hidden, this creates polished expand/collapse interactions:

import { motion } from 'framer-motion';
import { useState } from 'react';

export default function ExpandableCard() {
const [isExpanded, setIsExpanded] = useState(false);

return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
style={{
width: '200px',
backgroundColor: '#f0f0f0',
borderRadius: '8px',
padding: '20px',
cursor: 'pointer',
overflow: 'hidden',
}}
>
<motion.h3 layout>Card Title</motion.h3>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
This content appears when expanded. The card height animates smoothly.
</motion.p>
)}
</motion.div>
);
}

Here, toggling isExpanded changes the content. The layout prop on the container animates the height transition. The paragraph uses initial, animate, and exit for fade timing independent of layout.

Layout in Lists with Drag

Reorderable lists combine layout with drag handlers. Dragging an item reorders the state; layout animates all items to their new positions:

import { motion, Reorder } from 'framer-motion';
import { useState } from 'react';

export default function DraggableList() {
const [items, setItems] = useState(['Task 1', 'Task 2', 'Task 3']);

return (
<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
style={{ padding: '20px' }}
>
{items.map((item) => (
<Reorder.Item
key={item}
value={item}
style={{
padding: '10px',
backgroundColor: '#ddd',
marginBottom: '10px',
borderRadius: '4px',
cursor: 'grab',
}}
>
{item}
</Reorder.Item>
))}
</Reorder.Group>
);
}

Framer Motion's Reorder component wraps layout animation and drag handling. No extra code needed; drag and drop automatically reorder the list and animate positions.

Common Patterns

Pattern 1: Smooth column wrapping on resize

When a grid adapts from 4 columns to 2 columns at smaller viewports, items jump. With layout, they slide smoothly into the new grid:

<motion.div layout style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: '10px',
}}>
{items.map(item => <motion.div key={item.id} layout>{item.name}</motion.div>)}
</motion.div>

Pattern 2: Animated list filtering

When you filter a list (e.g., showing only "done" tasks), visible items animate to fill the gap left by hidden ones:

const visibleItems = items.filter(item => item.status === 'done');
<motion.ul style={{ listStyle: 'none' }}>
{visibleItems.map(item => (
<motion.li key={item.id} layout>
{item.title}
</motion.li>
))}
</motion.ul>

Performance Considerations

Layout animation is performant because it uses GPU-accelerated transform. However, animating too many elements simultaneously (>100) can cause jank on low-end devices. Profile with Chrome DevTools Performance tab: aim for 60 FPS (16.67 ms per frame).

For large lists, consider:

  • Virtualization: Only render visible items (react-window, react-virtual).
  • Batching: Stagger animations so not all items move at once.
  • Reduced motion: Respect prefers-reduced-motion by disabling layout animation on users' preference.

Key Takeaways

  • Layout animation detects DOM position/size changes and smoothly transitions between states.
  • Use the layout prop on motion components to enable automatic animation.
  • GPU-accelerated transform keeps animations fast; avoid CSS transition: all.
  • Combine layout with drag handlers for reorderable lists and interactive grids.
  • Profile performance on low-end devices and respect prefers-reduced-motion.

Frequently Asked Questions

Does layout animation work with CSS Grid and Flexbox?

Yes. Framer Motion measures the element's bounding box after each render, regardless of how the layout was created (Grid, Flexbox, absolute positioning). It animates the delta automatically.

Can I customize layout animation duration and easing?

Yes, with the transition prop: <motion.div layout transition={{ duration: 0.5, ease: 'easeInOut' }}>. Defaults are 0.25s ease-out.

Does layout animation cause layout thrashing?

No. Framer Motion batches measurements and animations using requestAnimationFrame, avoiding forced reflows. Performance is O(n) per frame regardless of list size.

What is layoutId and how does it differ from layout?

layout animates position changes within the same component tree. layoutId links two different components across routes or contexts for shared-element transitions (covered in article 3).

Can I pause or control layout animations programmatically?

Layout animations run automatically on state changes. To add custom controls, use AnimatePresence with custom onAnimationComplete callbacks or manual motion.animate() for imperative animation.

Further Reading