Documentation
Layout Animations
Animate position and size changes automatically with Motion's layout system -- how it works alongside motionwind classes.
Layout Animations
Layout animations are a Motion feature that automatically animates elements when their position or size changes in the DOM. Motionwind provides utility classes for Motion's layout animation system. These compile to the layout, layoutId, layoutScroll, and layoutRoot props.
This page covers the motionwind layout classes and explains each layout animation pattern, including how to use both the class-based approach and the direct Motion API for advanced cases.
Layout Classes
| Class | Motion output | Description |
|---|---|---|
animate-layout | layout | Animate position and size changes |
animate-layout-position | layout="position" | Only animate position changes |
animate-layout-size | layout="size" | Only animate size changes |
animate-layout-preserve | layout="preserve-aspect" | Preserve aspect ratio during animation |
animate-layout-id-{name} | layoutId="{name}" | Shared layout transition identifier |
animate-layout-scroll | layoutScroll | Account for scroll offset in layout animation |
animate-layout-root | layoutRoot | Mark as layout animation boundary |
Live Examples
Layout prop basics
The layout prop tells Motion to automatically animate an element whenever its layout (position or size) changes between renders. With motionwind, you can use the animate-layout class instead of the layout prop directly.
Class-based approach
<div className="animate-layout rounded-xl bg-[#1a1a2e] p-4">
<h3 className="animate-layout-position text-[#c8ff2e] font-bold">Card Title</h3>
</div>Compiles to:
<motion.div layout className="rounded-xl bg-[#1a1a2e] p-4">
<motion.h3 layout="position" className="text-[#c8ff2e] font-bold">Card Title</motion.h3>
</motion.div>Direct Motion API approach
For more complex cases where you need fine-grained control over transitions or conditional rendering, you can use the Motion API directly:
import { motion } from "motion/react";
function ExpandableCard({ isOpen }: { isOpen: boolean }) {
return (
<motion.div
layout
className="rounded-xl bg-[#1a1a2e] p-4"
style={{
width: isOpen ? 400 : 200,
height: isOpen ? 300 : 100,
}}
>
<motion.h3 layout="position" className="text-[#c8ff2e] font-bold">
Card Title
</motion.h3>
{isOpen && (
<p className="text-gray-400 mt-2">
Expanded content appears here.
</p>
)}
</motion.div>
);
}When isOpen toggles, the card smoothly animates between its two sizes. The layout prop on the outer div animates both position and size. The layout="position" on the heading means it only animates its position (not its size), preventing text from stretching.
How it works
Motion measures the element's bounding box before and after a React re-render. If the box changed, it uses a FLIP (First, Last, Invert, Play) animation to smoothly transition between the two states. You do not define start or end values -- Motion calculates them from the actual DOM layout.
Layout position changes
When elements reorder in a list, each element's position changes. The animate-layout class (or the layout prop directly) animates these position shifts.
import { useState } from "react";
import { motion } from "motion/react";
function SortableList() {
const [items, setItems] = useState(["Alpha", "Beta", "Gamma", "Delta"]);
const shuffle = () => {
setItems([...items].sort(() => Math.random() - 0.5));
};
return (
<div className="space-y-2">
{/* This button uses motionwind for hover/tap feedback */}
<button
className="animate-hover:scale-105 animate-tap:scale-95 animate-spring px-4 py-2 rounded-lg bg-[#c8ff2e] text-[#0a0a0f] font-semibold"
onClick={shuffle}
>
Shuffle
</button>
{items.map((item) => (
<motion.div
key={item}
layout
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="rounded-lg bg-[#1a1a2e] border border-[#c8ff2e]/10 px-4 py-3 text-[var(--color-fg)]"
>
{item}
</motion.div>
))}
</div>
);
}When the list shuffles, each motion.div smoothly slides to its new position. The button uses motionwind classes for hover and tap feedback. The Babel plugin transforms the button into a motion.button, while the list items use motion.div directly because they need the layout prop.
Layout size changes
The layout prop also animates size transitions. This is useful for expandable panels, accordions, and toggle states.
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
function ExpandablePanel() {
const [expanded, setExpanded] = useState(false);
return (
<motion.div
layout
onClick={() => setExpanded(!expanded)}
className="cursor-pointer rounded-xl bg-[#1a1a2e] border border-[#c8ff2e]/10 p-6 overflow-hidden"
style={{ width: expanded ? 400 : 200 }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
>
<motion.p layout="position" className="text-[#c8ff2e] font-medium">
Click to {expanded ? "collapse" : "expand"}
</motion.p>
<AnimatePresence>
{expanded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="mt-4 text-gray-400 text-sm"
>
This content fades in when the panel expands.
The panel itself animates its width smoothly.
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}The outer container animates its width change via layout. The inner text uses layout="position" so it stays anchored while the container resizes. The expanded content uses AnimatePresence for enter/exit opacity transitions.
Shared layout transitions (layoutId)
layoutId connects two separate elements across different render states. When one unmounts and another with the same layoutId mounts, Motion animates between their positions and sizes as if they were the same element.
With motionwind, you can use the animate-layout-id-{name} class to set a layoutId. For example:
animate-layout-id-active-tabcompiles tolayoutId="active-tab"animate-layout-id-card-herocompiles tolayoutId="card-hero"
Class-based tab example
{tabs.map((tab) => (
<button key={tab} onClick={() => setActiveTab(tab)} className="relative px-4 py-2 rounded-lg text-sm font-medium capitalize">
{activeTab === tab && (
<div className="animate-layout-id-active-tab-bg absolute inset-0 rounded-lg bg-[#c8ff2e]" />
)}
<span className="relative z-10">{tab}</span>
</button>
))}Compiles to:
{tabs.map((tab) => (
<button key={tab} onClick={() => setActiveTab(tab)} className="relative px-4 py-2 rounded-lg text-sm font-medium capitalize">
{activeTab === tab && (
<motion.div layoutId="active-tab-bg" className="absolute inset-0 rounded-lg bg-[#c8ff2e]" />
)}
<span className="relative z-10">{tab}</span>
</button>
))}Direct Motion API approach
For cases where you need custom transitions on shared layout elements, use the Motion API directly:
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
function TabBar() {
const [activeTab, setActiveTab] = useState("home");
const tabs = ["home", "about", "contact"];
return (
<div className="flex gap-2 p-2 rounded-xl bg-[#1a1a2e]">
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className="relative px-4 py-2 rounded-lg text-sm font-medium capitalize"
style={{ color: activeTab === tab ? "#0a0a0f" : "#a0a0b0" }}
>
{activeTab === tab && (
<motion.div
layoutId="active-tab-bg"
className="absolute inset-0 rounded-lg bg-[#c8ff2e]"
transition={{ type: "spring", stiffness: 300, damping: 25 }}
/>
)}
<span className="relative z-10">{tab}</span>
</button>
))}
</div>
);
}The green background element has layoutId="active-tab-bg". When activeTab changes, the background unmounts from one tab and mounts in another. Motion detects the matching layoutId and smoothly slides the background to the new position.
Gallery with shared layout
A common pattern is a grid of thumbnails that expand to a full detail view:
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
function Gallery({ items }: { items: { id: string; title: string; color: string }[] }) {
const [selected, setSelected] = useState<string | null>(null);
return (
<>
<div className="grid grid-cols-3 gap-4">
{items.map((item) => (
<motion.div
key={item.id}
layoutId={`card-${item.id}`}
onClick={() => setSelected(item.id)}
className="cursor-pointer rounded-xl p-6"
style={{ backgroundColor: item.color }}
>
<motion.h3 layoutId={`title-${item.id}`} className="font-bold text-[var(--color-fg)]">
{item.title}
</motion.h3>
</motion.div>
))}
</div>
<AnimatePresence>
{selected && (
<motion.div
layoutId={`card-${selected}`}
className="fixed inset-0 m-auto w-[500px] h-[400px] rounded-2xl p-8 z-50"
style={{
backgroundColor: items.find((i) => i.id === selected)?.color,
}}
onClick={() => setSelected(null)}
>
<motion.h3
layoutId={`title-${selected}`}
className="font-bold text-[var(--color-fg)] text-2xl"
>
{items.find((i) => i.id === selected)?.title}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ delay: 0.2 }}
className="text-[var(--color-fg-muted)] mt-4"
>
Click to close this expanded view.
</motion.p>
</motion.div>
)}
</AnimatePresence>
</>
);
}Each thumbnail and its expanded form share a layoutId. When a thumbnail is clicked, the card appears to grow from the thumbnail position to the full-size overlay.
Reorder animations
Motion provides a Reorder component for drag-to-reorder lists. Each item animates to its new position automatically.
import { useState } from "react";
import { Reorder } from "motion/react";
function ReorderList() {
const [items, setItems] = useState(["Item A", "Item B", "Item C", "Item D"]);
return (
<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
className="space-y-2 w-64"
>
{items.map((item) => (
<Reorder.Item
key={item}
value={item}
className="cursor-grab active:cursor-grabbing rounded-lg bg-[#1a1a2e] border border-[#c8ff2e]/10 px-4 py-3 text-[var(--color-fg)] select-none"
whileDrag={{
scale: 1.03,
boxShadow: "0 8px 24px rgba(200, 255, 46, 0.15)",
}}
>
{item}
</Reorder.Item>
))}
</Reorder.Group>
);
}Reorder.Group manages the list state and Reorder.Item wraps each draggable element. Motion automatically animates position changes when items are reordered. The whileDrag prop adds visual feedback during the drag.
Mixing Reorder with motionwind
motionwind's animate-drag-* classes work for basic drag behavior on motion.* elements, but Reorder has its own drag management built in. Use motionwind classes on elements outside the reorder list:
<div>
{/* motionwind button outside the Reorder list */}
<button className="animate-hover:scale-105 animate-tap:scale-95 animate-spring mb-4 px-4 py-2 rounded-lg bg-[#c8ff2e] text-[#0a0a0f] font-semibold">
Reset Order
</button>
<Reorder.Group axis="y" values={items} onReorder={setItems}>
{/* Reorder items use Motion's Reorder API directly */}
{items.map((item) => (
<Reorder.Item key={item} value={item} className="...">
{item}
</Reorder.Item>
))}
</Reorder.Group>
</div>Grid layout transitions
Combine CSS Grid with the layout prop to animate items as the grid changes shape.
import { useState } from "react";
import { motion } from "motion/react";
function DynamicGrid() {
const [columns, setColumns] = useState(3);
const items = Array.from({ length: 9 }, (_, i) => i + 1);
return (
<div>
{/* Toggle button with motionwind classes */}
<button
className="animate-hover:scale-105 animate-tap:scale-95 animate-spring mb-4 px-4 py-2 rounded-lg bg-[#c8ff2e] text-[#0a0a0f] font-semibold"
onClick={() => setColumns(columns === 3 ? 2 : 3)}
>
Toggle {columns === 3 ? "2" : "3"} columns
</button>
<div
className="grid gap-3"
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
>
{items.map((item) => (
<motion.div
key={item}
layout
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="rounded-xl bg-[#1a1a2e] border border-[#c8ff2e]/10 p-6 text-center text-[var(--color-fg)] font-bold"
>
{item}
</motion.div>
))}
</div>
</div>
);
}When the column count changes, each grid item smoothly animates to its new position and size. The layout prop on each motion.div handles the measurement and animation automatically.
Layout Scroll
The animate-layout-scroll class compiles to the layoutScroll prop. Use it on scrollable containers so that Motion correctly accounts for scroll offset when measuring element positions during layout animations.
Without this, layout animations inside a scrollable container may appear to jump because Motion measures positions relative to the viewport, not the scroll container.
<div className="animate-layout-scroll overflow-auto h-64">
<div className="animate-layout rounded-lg bg-[#1a1a2e] p-4">
Content that animates correctly inside a scrollable container.
</div>
</div>Compiles to:
<motion.div layoutScroll className="overflow-auto h-64">
<motion.div layout className="rounded-lg bg-[#1a1a2e] p-4">
Content that animates correctly inside a scrollable container.
</motion.div>
</motion.div>Layout Root
The animate-layout-root class compiles to the layoutRoot prop. It creates an animation boundary that prevents layout animations from propagating above this point in the component tree.
This is useful when you want to isolate a section of your page so that layout changes inside it do not trigger layout recalculations in parent components.
<div className="animate-layout-root">
<div className="animate-layout rounded-lg bg-[#1a1a2e] p-4">
Layout animations inside here are isolated from the rest of the page.
</div>
</div>Compiles to:
<motion.div layoutRoot>
<motion.div layout className="rounded-lg bg-[#1a1a2e] p-4">
Layout animations inside here are isolated from the rest of the page.
</motion.div>
</motion.div>When to use layout animations vs motionwind
| Need | Approach |
|---|---|
| Hover, tap, focus, scroll effects | motionwind classes (animate-hover:*, etc.) |
| Enter/exit transitions | motionwind classes (animate-initial:*, animate-enter:*, animate-exit:*) |
| Animating DOM position/size changes | motionwind class (animate-layout) or Motion layout prop |
| Shared element transitions | motionwind class (animate-layout-id-{name}) or Motion layoutId prop |
| Scroll-aware layout animations | motionwind class (animate-layout-scroll) or Motion layoutScroll prop |
| Layout animation boundaries | motionwind class (animate-layout-root) or Motion layoutRoot prop |
| Drag-to-reorder lists | Motion Reorder component directly |
| Grid/list reflow animations | motionwind class (animate-layout) or Motion layout prop on each item |
Layout animations and motionwind classes now work hand-in-hand. Use motionwind layout classes (animate-layout, animate-layout-id-{name}, etc.) for straightforward layout animations where you want to stay in the utility-class workflow. Use the Motion API directly when you need fine-grained control over transitions, conditional rendering with AnimatePresence, or specialized components like Reorder.