Choosing the Right Variants Pattern for Framer Motion Component Transitions
Decide how to structure Framer Motion variants for React component transitions: inline, memoized, or external. Learn constraints, trade‑offs, and see concrete examples with exit and layout animations.
28 Aug 2025, 03:24 UTC

Problem & Takeaway
When building React UIs with Framer Motion, developers often face a decision: how to structure animation variants for component transitions. The core issue is balancing clarity and performance while ensuring compatibility with server‑side rendering (SSR) and AnimatePresence exit animations. The practical takeaway is to define variants outside the render tree—preferably with useMemo or in a separate module—and wrap conditional content in AnimatePresence to guarantee exit variants fire correctly.
Decision Space
- Define variants inline – quick, but can cause unnecessary re‑renders and SSR hydration mismatches.
- Memoize variants with
useMemo– avoids re‑creation on every render, but still ties them to the component’s lifecycle. - External variant modules – promotes reuse and testability, but adds import overhead.
- Use
layoutprop for shared layout transitions – requires container awareness but eliminates manual key management. - Leverage
whileHover/whileTapfor interaction states – useful for buttons but not for structural transitions.
Constraints
- SSR: Motion components render without animation on the server to avoid hydration errors.
- Re‑renders: Inline variants create new objects on each render, triggering motion updates even when props don’t change.
- Exit animations:
AnimatePresencemust wrap the conditional element and the element must have a stablekey. - Layout changes: When items are added or removed,
layoutprop must be set on the container to animate positions.
Compact Comparison Table
| Approach | Pros | Cons | Typical Use Case |
|---|---|---|---|
| Inline variants | Fast to write, no extra state. | Re‑creates objects every render; SSR hydration mismatch risk. | Small, static components with no dynamic state. |
| useMemo variants | Stable references; avoids unnecessary updates. | Still tied to component; needs dependency array. | Reusable animation logic within a component. |
| External module | Single source of truth; easy testing. | Import overhead; can’t capture component props. | Shared animations across many components. |
| layout prop | Automatic position transitions. | Requires container awareness; may cause layout thrashing. | Lists, grids, or any DOM structure that changes. |
| whileHover / whileTap | Declarative interaction states. | Not suitable for structural transitions. | Buttons, cards, any interactive element. |
Trade‑off Analysis
- Performance vs. Simplicity: Inline variants are simple but can degrade performance on large trees. Memoizing or externalizing variants gives better performance at the cost of a slightly more complex setup.
- SSR Compatibility vs. Flexibility: To avoid hydration mismatches, always disable animation on the server by setting
initial={false}or usingmotionConfig={{ defaultTransition: { duration: 0 } }}during SSR. - Exit Animations vs. Conditional Rendering: Without
AnimatePresence, exit variants never run. Ensure every conditional component is wrapped and has a stablekey. - Layout Animations vs. Manual Key Management: Using
layoutremoves the need to manually setkeyon children, but the container must be motion‑aware.
Concrete Implementation
Example 1: Toggle Button with Variants
We’ll create a button that toggles between idle and pressed states using a memoized variants object.
import { motion, useAnimation } from "framer-motion";
import { useMemo } from "react";
const buttonVariants = useMemo(() => ({
idle: { scale: 1, backgroundColor: "#3498db" },
pressed: { scale: 0.95, backgroundColor: "#2980b9" }
}), []);
export function ToggleButton() {
const controls = useAnimation();
return (
<motion.button
variants={buttonVariants}
initial="idle"
animate={controls}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }}
onClick={() => controls.set(controls.get() === "idle" ? "pressed" : "idle")}
>
Toggle
</motion.button>
);
}
**Verification**: Run the app, click the button. The button should smoothly scale and change color. Inspect the element’s style to ensure the scale and backgroundColor values match the variant definitions.
Example 2: Exit Animation with AnimatePresence
We’ll conditionally render a card that fades out on unmount.
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3 } }
};
export function CardToggle() {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(s => !s)}>Toggle Card</button>
<AnimatePresence>
{show && (
<motion.div
key="card"
variants={cardVariants}
initial="hidden"
animate="visible"
exit="hidden"
style={{ width: 200, height: 100, background: "#e74c3c", margin: 20 }}
>
Card Content
</motion.div>
)}
</AnimatePresence>
</div>
);
}
**Verification**: Click the button to hide the card. The card should animate to opacity: 0 and slide down before unmounting. Inspect the DOM to confirm the key="card" attribute remains stable across renders.
Example 3: Shared Layout Transition
Animating list items when one is removed.
import { motion } from "framer-motion";
import { useState } from "react";
export function ItemList() {
const [items, setItems] = useState([1, 2, 3, 4]);
return (
<motion.ul layout style={{ listStyle: "none", padding: 0 }}>
{items.map(id => (
<motion.li
key={id}
layout
style={{ padding: 10, margin: 5, background: "#ecf0f1" }}
>
Item {id}
<button onClick={() => setItems(i => i.filter(x => x !== id))}>X</button>
</motion.li>
))}
</motion.ul>
);
}
**Verification**: Remove an item. The remaining items should smoothly shift into the vacated space. Use the browser’s dev tools to confirm that the layout prop is present on each li and that the ul is motion‑enabled.
Limitations & Practical Checks
- SSR: If you’re using Next.js or another SSR framework, disable animations on the first render by setting
initial={false}or usingmotionConfigto zero out transitions. - Re‑renders: Memoize or externalize variants to avoid new object creation on every render. Verify by checking the reference equality in dev tools.
- Exit support: Remember that
AnimatePresenceonly works withReactkeychanges. Test by toggling a state that changes thekeyvalue. - Layout thrashing: Using
layouton many items can cause performance hits. Profile with React DevTools Profiler.
Conclusion
For production‑grade Framer Motion animations, define variants outside the render tree—using useMemo or a shared module—and wrap conditional components in AnimatePresence with stable keys. This pattern keeps the UI responsive, avoids SSR hydration issues, and provides a clean separation between animation logic and component rendering.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.