Coordinate React animations with Framer Motion variants
Use Framer Motion variants to declaratively animate parent and child elements together without managing individual animate props on every node.
18 Oct 2025, 12:21 UTC

The problem: animating a list one item at a time
Imagine a task panel that opens to reveal a list of items. You want each item to fade in and slide up, but not all at once—you want a slight stagger so the list feels deliberate. Without a shared animation strategy, you end up adding an animate prop to every motion.li, tracking the same boolean in multiple places, and hand-wiring delays. As the list grows, that boilerplate becomes hard to maintain.
Framer Motion's variants solve this by letting you define named animation states—such as "hidden" and "visible"—and apply them from a parent component to all child motion elements. A motion component is a Framer Motion wrapper (like motion.div) that can animate its styles declaratively.
How variants propagate state
When you attach a variants object to a parent motion component and change its animate prop, Framer Motion broadcasts that state to child motion components. Children that define matching variant names animate automatically. You do not need to pass animate to every child.
This works with layout animations, hover states, and transitions. You can also nest variants: a parent can trigger "visible", and each child can interpret that same name differently—one fades, another slides, another scales.
Worked example: a staggered task list
This example assumes Framer Motion 10 or later. The parent motion.ul owns the variant state. The child motion.li elements declare their own variants but rely on the parent for the active state.
import { useState } from "react";
import { motion } from "framer-motion";
const listVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.08 },
},
};
const itemVariants = {
hidden: { y: 16, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.25, ease: "easeOut" },
},
};
export default function TaskPanel() {
const [open, setOpen] = useState(false);
const tasks = [
{ id: 1, label: "Review pull request" },
{ id: 2, label: "Update design specs" },
{ id: 3, label: "Deploy to staging" },
];
return (
setOpen((o) => !o)}>
{open ? "Collapse tasks" : "Open tasks"}
{tasks.map((task) => (
{task.label}
))}
);
}
When open flips to true, the parent's animate becomes "visible". Framer Motion tells each child motion.li to enter its "visible" variant. Because listVariants.visible includes staggerChildren: 0.08, the items animate in sequence rather than all at once.
Where to verify the behavior
- Open React DevTools, select the
motion.ulelement, and confirm itsanimateprop equals"visible"when the panel is open. - Toggle the button and confirm each
motion.lifades and moves upward in sequence. - If nothing moves, check that every animated element uses a motion component—
ulandliwill not work; they must bemotion.ulandmotion.li. - Confirm variant names match exactly. Framer Motion silently ignores variant names that are not defined on the component, so a typo like
"visable"produces no error and no animation.
Trade-offs and limitations
- Motion components only. You cannot attach variants to a native DOM element or animate arbitrary props such as
className. - Silent failures. Referencing an undefined variant or prop is ignored at runtime, which can mask typos during refactors.
- Deep nesting can obscure bugs. If you nest variants many levels deep, tracing which parent triggered a child's state becomes harder. Prefer flat variant structures where practical.
- Dynamic values need
custom. If a variant must read a runtime prop, pass it through thecustomprop and read it inside a variant function rather than hard-coding values.
Closing
Variants are most valuable when several related elements share a lifecycle. Instead of scattering animation state across your component tree, define it once on a parent and let motion components respond. Start with a single list or panel, verify the animate prop in DevTools, and expand from there.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.