Choosing an Animation Strategy for Modals in Framer Motion: Variants vs. Direct Props vs. useAnimation
When building a modal with Framer Motion, pick the right animation strategy—Variants for reusable states, direct props for quick one‑offs, or useAnimation for event‑driven sequences. This guide compares the options, explains trade‑offs, and shows a concrete implementation.
01 Jun 2026, 07:55 UTC

Problem & Decision Focus
When building a modal component that slides in and fades out, you need to decide which Framer Motion technique best fits your goals. The three common options are:
- Variants – a declarative state machine that maps named states to style objects.
- Direct prop animations – passing an
animateobject directly to a component. - useAnimation – an imperative controller that lets you trigger animations from code.
Each approach offers a different balance of maintainability, reusability, and event‑driven control. This guide helps you pick the right one for a modal entrance use‑case.
Constraints to Consider
- Reusability – Will the same animation logic be applied to multiple modals or nested components?
- Event‑driven timing – Does the animation need to respond to external events (e.g., API response, user interaction) after the component mounts?
- Developer ergonomics – How much boilerplate is acceptable for the team?
- Performance & bundle size – Are there any measurable differences in runtime or build output?
- Debuggability – Are you comfortable with implicit state changes or do you need explicit control?
Comparison Table
| Approach | Use Case | Boilerplate | Reusability | Event Control | Performance Impact | Bundle Size | Common Pitfalls |
|---|---|---|---|---|---|---|---|
| Variants | Consistent open/close states across many modals | Medium – define a variants object once | High – share the same variants object | Low – state changes are triggered by animate prop changes | Neutral – same as other methods | Neutral – no extra code | Missing variant names, mismatched state names |
| Direct props | Simple, one‑off animation (e.g., a tooltip fade) | Low – inline animate object | Low – logic duplicated per component | Low – triggered only on mount/unmount | Neutral | Neutral | Duplicate logic, hard to maintain |
| useAnimation | Complex, event‑driven sequences (e.g., waiting for data) | High – controller setup, imperative calls | Medium – controller can be passed down | High – can start/stop on any event | Neutral | Neutral | Stale closures, forgetting to reset controller |
Trade‑Off Analysis
- Variants give you a clean, declarative API. The
animateprop simply references a variant name, and Framer Motion handles the transition logic. This is ideal when the modal’s open/close states are static and shared across the app. The main risk is silent failures if a variant name is misspelled – no warning is thrown, so the animation just doesn’t run. - Direct props are the fastest to write but can lead to scattered animation logic. They’re suitable for small, isolated components where you don’t plan to reuse the animation. If you later need to adjust the fade timing, you’ll have to touch every instance.
- useAnimation shines when you need to coordinate multiple animations or tie them to asynchronous events. The imperative controller allows you to start, stop, or chain animations on demand. However, it introduces more boilerplate and the risk of stale closures if you’re not careful with memoization.
Concrete Implementation: Modal with Variants
Below is a minimal React component that demonstrates a modal using variants for its open/close states. The example uses Framer Motion 10.x and React 18.
// modal-variants.jsx
import { motion } from "framer-motion";
import { useState } from "react";
const modalVariants = {
hidden: { opacity: 0, y: -50, transition: { duration: 0.3 } },
visible: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 300, damping: 30 } },
};
export function Modal({ isOpen, onClose, children }) {
return (
<motion.div
className="modal-backdrop"
initial="hidden"
animate={isOpen ? "visible" : "hidden"}
variants={modalVariants}
onClick={onClose}
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<motion.div
className="modal-content"
onClick={(e) => e.stopPropagation()}
style={{ background: "white", padding: 20, borderRadius: 8 }}
>
{children}
</motion.div>
</motion.div>
);
}
Usage in an app:
// App.jsx
import { useState } from "react";
import { Modal } from "./modal-variants";
export default function App() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal isOpen={open} onClose={() => setOpen(false)}>
<p>Hello from the modal!</p>
</Modal>
</div>
);
}
Validation Checklist
- Run the app:
npm start(requiresreact-scriptsor similar). - Open the modal and observe the slide‑in animation. Verify that the
visiblevariant is applied. - Close the modal by clicking the backdrop; the
hiddenvariant should animate out. - Open Chrome DevTools Performance panel, record a session, and check that the frame rate stays above 60 fps during the transition.
- Look for any console warnings about missing variants or animation errors.
Alternative: Use Direct Props
For a quick prototype, you can replace the variants logic with an inline animate object:
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
...
</motion.div>
This removes the need for a separate variants object but duplicates the style object if you use it elsewhere.
Alternative: useAnimation Hook
When the modal entrance must wait for an async event (e.g., data fetch), use the useAnimation hook:
import { useAnimation, motion } from "framer-motion";
import { useEffect } from "react";
export function AsyncModal({ isOpen, onClose }) {
const controls = useAnimation();
useEffect(() => {
if (isOpen) {
controls.start("visible");
} else {
controls.start("hidden");
}
}, [isOpen, controls]);
return (
<motion.div
animate={controls}
initial="hidden"
variants={{
hidden: { opacity: 0, y: -50 },
visible: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 300, damping: 30 } },
}}
onClick={onClose}
>
...
</motion.div>
);
}
Here controls is an imperative controller; you can trigger it from anywhere, even outside the component, by passing the controller up the tree.
Final Decision Checklist
- Do you need a reusable, declarative state machine across many components? Choose Variants.
- Is the animation a one‑off effect with no future reuse? Choose Direct Props.
- Does the animation depend on external events or complex sequencing? Choose useAnimation.
All three approaches are lightweight and have similar bundle size impact. Prefer the simpler method that meets your constraints to keep the codebase maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.