Coordinated Enter/Exit Animations with Framer Motion Variants
Framer Motion's variants API lets you declare enter and exit sequences declaratively, replacing imperative timers with a predictable animation contract in React.
18 Sept 2025, 19:35 UTC

The Problem with Chaotic Entrances
Building a React component grid or list where each item animates on mount and fades out when unmounted often becomes a tangle of useEffect timers, random delay values, and inconsistent easing functions. Without a shared animation contract, items may play out of order, restart unexpectedly, or fail to coordinate their exit when unmounting, leaving the UI feeling disjointed.
What Variants Actually Do
Framer Motion's variants API lets you define named animation states—initial, animate, and exit—in a single plain object. Child motion components reference that object via their animate or exit prop. The transition property inside each state controls duration, delay, type, and easing. These values inherit down to children, and you can override them per child if needed. Because the transition values are inherited, you only need to define them once at the parent level, and every child respects the same timing curve. This declarative structure means Framer Motion resolves the sequence internally, eliminating the need for imperative timers or manual setTimeout calls.
A Minimal Working Example
import { motion } from "framer-motion";
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, type: "easeOut" },
},
exit: {
opacity: 0,
y: -20,
transition: { duration: 0.3, type: "easeIn" },
},
};
const Gallery = () => (
Item 1
Item 2
Item 3
);
Each child inherits the same variant object. Adding transition.delayChildren to the parent staggers the entry gap between siblings, while the exit prop ensures each item animates out automatically when it unmounts. The hidden state is applied initially, so items fade in as they mount.
Controlling Variants Programmatically
When user-driven timing is needed, the useAnimation hook provides manual control:
import { useAnimation } from "framer-motion";
const InteractiveGallery = () => {
const controls = useAnimation();
return (
controls.start("visible")}>Animate in
controls.start("exit")}>Remove item
Dynamic item
);
};
The controls prop propagates down to every child, so individual elements can be targeted with controls.start("exit"), or you can sequence multiple animations. The variant names "visible" and "exit" remain the same; only the controller changes.
Trade-offs and Gotchas
- Stable definitions: Variant objects must be defined outside the component body or wrapped in a stable ref. Inline definitions or those created inside
useCallbackwithout proper dependencies can cause the variant to reset on every render, breaking the animation sequence. - Exit-once behavior: Exit animations fire only once per element removal. Re-adding the same component without a key change may not re-trigger the exit sequence, since Framer Motion tracks the element's unmount history.
- Stagger control: Adding
transition.delayChildrento the parent controls the gap between child entries, but excessive delays can make the UI feel sluggish. ThestaggerChildrenprop offers automatic spacing, but may require fine-tuning for your layout.
Verifying in React DevTools
To check your setup, open React DevTools, select any motion component in the tree, and inspect the animate prop. It should reflect the current variant state (e.g., visible or exit). The variants object appears in the component props; verify that the name matches exactly between parent and child. If an animation seems off, confirm that the variant definition isn't being recreated on each render and that no stray key props are causing unintended remounts.
Quick check pattern: Place the variant object at the top of your module file (not inside the component function). Add animate="visible" to each child that should enter. In DevTools, the animate prop value should change to visible and the element should fade/slide as defined. If it jumps or resets, the variant definition likely lacks stability.
If the animation does not play, check the console for Framer Motion warnings about missing variant names or mismatched state keys.
Wrap-up and Next Steps
- Define your variant object at the top level of a module or inside a
useRefso it stays stable across renders. - Pass the same
variantsobject to all children that share the animation lifecycle. - Use
transition.delayChildrenfor manual stagger gaps ortransition.staggerChildrenfor automatic spacing. - Open React DevTools, select the
motioncomponent, and verify that theanimateprop cycles through the expected variant states. - Experiment with
whileInLayoutfor layout-aware transitions orcontrolsfor choreographed sequences that respond to user input.
From there, you can layer on more complex patterns, such as direction-aware stagers or syncing animation state with form submissions, but the fundamentals above will keep your enter/exit sequences predictable and maintainable.