Framer Motion Variants and Layout Animation: Declarative Choreography Without the Boilerplate
Framer Motion's variants and layout props centralize animation logic and automate FLIP transitions. This blog shows how to replace scattered keyframes with reusable variant objects, enable layout animation for reordering grids, and avoid common SSR and bundle-size pitfalls.
10 Aug 2026, 09:07 UTC

The problem: scattered animation logic
React components often end up with inline transition objects, duplicated keyframe arrays, and imperative useEffect hooks that trigger animations on mount. When a design system requires consistent easing, stagger delays, or shared layout transitions across a card grid, a modal, and a navigation drawer, the duplication becomes a maintenance burden. Framer Motion’s variants and layout props solve this by moving animation definitions into reusable objects and letting the library measure layout changes automatically.
Variants: declarative orchestration
A variant is a plain object that maps a label (like "visible" or "hidden") to a target style and optional transition settings. You pass the variant object to a motion component’s variants prop, then reference its keys in animate, initial, or exit. Child motion components that also declare variants inherit the parent’s animation state, enabling stagger and sequencing without manual delay calculations.
Basic variant structure
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeOut" }
},
exit: { opacity: 0, y: -20, transition: { duration: 0.2 } }
};Use it on a parent and children:
<motion.div variants={cardVariants} initial="hidden" animate="visible" exit="exit">
{items.map(item => (
<motion.div key={item.id} variants={cardVariants} />
))}
</motion.div>The parent’s animate="visible" triggers the children’s visible variant automatically. Add transition: { staggerChildren: 0.1 } to the parent’s visible variant to sequence them.
Layout animation: automatic shared-element transitions
The layout prop tells Framer Motion to measure a component’s position and size on every render, then animate between the previous and current layout using a performant FLIP (First, Last, Invert, Play) technique. No manual keyframes or coordinate math required.
Enabling layout animation
<motion.div layout className="card" />When a sibling is removed, a list reorders, or a parent resizes, the remaining layout components animate to their new positions. Combine with layoutId to morph one element into another across route changes or conditional renders:
{showDetail ? (
<motion.div layoutId="card" className="detail" />
) : (
<motion.div layoutId="card" className="thumbnail" />
)}The library cross-fades and transforms the shared layoutId element, preserving visual continuity.
Worked example: animated card grid with filter
Imagine a dashboard showing project cards. Users can filter by tag, and cards should reflow smoothly. The grid uses CSS Grid; each card is a motion.div with layout. A parent AnimatePresence handles enter/exit animations via variants.
Component code
const gridVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.05, delayChildren: 0.1 }
}
};
const cardVariants = {
hidden: { opacity: 0, scale: 0.95 },
visible: { opacity: 1, scale: 1, transition: { type: "spring", stiffness: 300 } },
exit: { opacity: 0, scale: 0.95, transition: { duration: 0.15 } }
};
function ProjectGrid({ projects, selectedTag }) {
const filtered = selectedTag
? projects.filter(p => p.tags.includes(selectedTag))
: projects;
return (
<motion.div
className="grid"
variants={gridVariants}
initial="hidden"
animate="visible"
>
<AnimatePresence mode="popLayout">
{filtered.map(project => (
<motion.div
key={project.id}
layout
variants={cardVariants}
className="card"
>
<h3>{project.name}</h3>
<p>{project.description}</p>
</motion.div>
))}
</AnimatePresence>
</motion.div>
);
}What happens
- Filter changes →
filteredarray updates. AnimatePresence mode="popLayout"keeps exiting cards in the DOM long enough to run theirexitvariant while remaining cards uselayoutto animate to new grid positions.- Entering cards run the
visiblevariant with a spring; the parent’sstaggerChildrenspaces them by 50 ms.
No manual height calculations, no requestAnimationFrame loops. The browser devtools style inspector will show transform: translate3d(...) scale(...) on each card during the transition, confirming FLIP is active.
Trade-offs and limitations
- Bundle size: Deeply nested variant objects increase the JavaScript payload. Keep variant definitions shallow; extract shared transitions into constants.
- SSR hydration mismatches: If
AnimatePresencekeys or initial variant states differ between server and client, React will warn. Ensure the sameinitialvariant renders on both sides, or delay animation until after hydration withuseEffect(() => { setMounted(true) }, []). - Variant redefinition: Creating new variant objects inline (e.g.,
variants={{ ... }}) on every render resets animation state. Define variants outside the component or memoize withuseMemo. - Layout measurement cost:
layoutreadsgetBoundingClientRecton each render. For hundreds of elements, considerlayout={false}on static items or use thelayoutDependencyprop to limit recalculations.
Actionable next steps
- Identify one animation pattern repeated across your codebase (e.g., fade-in-up on mount). Extract it into a variant constant.
- Replace a manual list-reorder animation with
layouton the list items andAnimatePresence mode="popLayout"on the parent. - Run a production build and check the network tab for Framer Motion’s chunk size; if it exceeds your budget, audit variant depth and consider
lazymotion components. - Add a CI step that runs
npm run build && node scripts/ssr-check.js(a script that renders the app withReactDOMServer) to catch hydration warnings early.
Start small: one grid, one modal, one shared-element transition. The declarative model pays off when the design system asks for a new easing curve and you change it in one place.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.