Variants & Orchestration: Staggering Complex Animations
Variants in Framer Motion let you define named animation states and apply them to multiple elements, creating synchronized, choreographed animations. With staggerChildren and delayChildren, you can orchestrate complex sequences where dozens of elements animate in perfect timing. This is the pattern used in production dashboards and design tools to create those smooth, professional entrance animations.
I've used variants to orchestrate card reveals, list animations, and modal enterings with precise timing. The variant pattern scales from simple two-state animations to elaborate sequences, all defined declaratively.
What Are Variants?
Variants are reusable animation state objects. Instead of inline initial, animate, and exit props, you define named states and reference them:
import { motion } from 'framer-motion';
const containerVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
};
export function SimpleVariants() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
transition={{ duration: 0.6 }}
style={{
padding: '20px',
backgroundColor: '#f0f0f0',
borderRadius: '8px'
}}
>
Animated with variants
</motion.div>
);
}
The initial and animate props now reference variant names ("hidden", "visible") instead of inline objects. This is cleaner when you reuse the same animation shape across multiple components.
Parent-Child Variant Propagation
The real power of variants emerges when you combine parent and child components. A parent's animate state automatically propagates to children with the same variant names:
import { motion } from 'framer-motion';
const parentVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1 // Delay each child by 0.1s
}
}
};
const childVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
export function VariantList() {
const items = ['Apple', 'Banana', 'Cherry'];
return (
<motion.ul
variants={parentVariants}
initial="hidden"
animate="visible"
style={{ listStyle: 'none', padding: 0 }}
>
{items.map((item) => (
<motion.li
key={item}
variants={childVariants}
transition={{ duration: 0.4 }}
style={{
padding: '12px',
borderBottom: '1px solid #eee',
backgroundColor: '#fff'
}}
>
{item}
</motion.li>
))}
</motion.ul>
);
}
When the parent animates from "hidden" to "visible", each child animates the same way, but with a 0.1-second stagger. The first child starts immediately, the second after 0.1s, the third after 0.2s, creating a cascade effect.
Staggering and Orchestrating Sequences
The transition object in a parent variant controls child sequencing via two key properties:
staggerChildren: Delay between each child animation start (e.g., 0.1 = 100ms).delayChildren: Initial delay before the first child animates (e.g., 0.2 = 200ms before any child moves).
import { motion } from 'framer-motion';
const containerVariants = {
hidden: {},
visible: {
transition: {
delayChildren: 0.3, // Wait 0.3s before first child animates
staggerChildren: 0.2 // Each subsequent child delays 0.2s more
}
}
};
const itemVariants = {
hidden: { opacity: 0, scale: 0 },
visible: { opacity: 1, scale: 1, transition: { duration: 0.4 } }
};
export function GridAnimation() {
const items = Array.from({ length: 9 }, (_, i) => i + 1);
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 100px)',
gap: '15px',
padding: '20px'
}}
>
{items.map((item) => (
<motion.div
key={item}
variants={itemVariants}
style={{
width: '100px',
height: '100px',
backgroundColor: '#3498db',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontWeight: 'bold'
}}
>
{item}
</motion.div>
))}
</motion.div>
);
}
The grid displays a 3x3 grid of squares. With delayChildren: 0.3, nothing moves for 300ms, then the first square scales up. With staggerChildren: 0.2, each subsequent square starts 200ms after the previous. The entire sequence takes about 2 seconds (0.3 delay + 8 staggered items at 0.2s each).
Variant Inheritance and Overrides
Child components inherit transition settings from parents, but can override them:
import { motion } from 'framer-motion';
const containerVariants = {
visible: {
transition: { staggerChildren: 0.1 }
}
};
const childVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
};
export function OverrideExample() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
<motion.div
variants={childVariants}
transition={{ duration: 0.5 }} // Override default timing
style={{ padding: '10px', backgroundColor: '#f0f0f0' }}
>
Slower child animation
</motion.div>
<motion.div
variants={childVariants}
transition={{ duration: 0.2 }} // Different timing
style={{ padding: '10px', backgroundColor: '#f0f0f0' }}
>
Faster child animation
</motion.div>
</motion.div>
);
}
Each child gets a different transition duration, but all are sequenced with the parent's stagger.
Complex Multi-Level Variant Sequences
Variants work at any nesting depth. You can orchestrate a variant sequence across a header, multiple card sections, and footers:
import { motion } from 'framer-motion';
const pageVariants = {
initial: {},
animate: {
transition: {
staggerChildren: 0.2,
delayChildren: 0.1
}
}
};
const sectionVariants = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 }
};
export function PageLayout() {
return (
<motion.div
variants={pageVariants}
initial="initial"
animate="animate"
>
<motion.header
variants={sectionVariants}
style={{ padding: '20px', borderBottom: '1px solid #eee' }}
>
<h1>Page Title</h1>
</motion.header>
<motion.main variants={sectionVariants} style={{ padding: '20px' }}>
<p>Main content appears here with staggered timing.</p>
</motion.main>
<motion.footer
variants={sectionVariants}
style={{ padding: '20px', borderTop: '1px solid #eee' }}
>
<p>Footer animates last in sequence.</p>
</motion.footer>
</motion.div>
);
}
The page itself is a container with staggerChildren. Each section (header, main, footer) is a child variant that animates after the previous section. This is the pattern used in professional landing pages and dashboards.
Using Variants with Conditional Rendering
Combine variants with React state to create interactive animations:
import { motion, AnimatePresence } from 'framer-motion';
import React from 'react';
const listVariants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.05 }
}
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
exit: { opacity: 0, x: 20 }
};
export function FilteredList() {
const [filter, setFilter] = React.useState('all');
const items = [
{ id: 1, label: 'Task 1', category: 'work' },
{ id: 2, label: 'Task 2', category: 'personal' },
{ id: 3, label: 'Task 3', category: 'work' }
];
const filtered = items.filter(
(item) => filter === 'all' || item.category === filter
);
return (
<div>
<div style={{ marginBottom: '20px' }}>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('work')}>Work</button>
<button onClick={() => setFilter('personal')}>Personal</button>
</div>
<AnimatePresence>
<motion.ul
key={filter}
variants={listVariants}
initial="hidden"
animate="visible"
exit="hidden"
style={{ listStyle: 'none', padding: 0 }}
>
{filtered.map((item) => (
<motion.li
key={item.id}
variants={itemVariants}
exit="exit"
style={{
padding: '10px',
borderBottom: '1px solid #eee'
}}
>
{item.label}
</motion.li>
))}
</motion.ul>
</AnimatePresence>
</div>
);
}
When the filter changes, the list re-animates. Filtered-out items exit with an animation, and remaining items re-stagger in.
Key Takeaways
- Variants are named animation states that you reference instead of writing inline props.
- Parent variants propagate to children automatically, enabling synchronized sequences.
- Use
staggerChildrento delay each child by a fixed interval; usedelayChildrento delay the first child. - Variants work at any nesting depth, supporting complex multi-level orchestration.
- Combine variants with
AnimatePresencefor entrance/exit sequences on conditional rendering.
Frequently Asked Questions
Can I animate child elements independently of the parent's variants?
Yes. Set initial and animate on a child to override parent propagation. Or use a different variant name that the parent doesn't control. Children's transition prop also overrides parent settings.
How do I calculate the total animation time with staggerChildren?
Total time = delayChildren + (number of children - 1) * staggerChildren + child animation duration. For 5 children, delayChildren: 0.2, staggerChildren: 0.1, and duration: 0.4: total is 0.2 + 4*0.1 + 0.4 = 0.9 seconds.
Can I use variants with SVG elements?
Yes. Define variant objects and apply them to motion.svg, motion.path, motion.circle, etc. The same stagger and orchestration logic works.
What if I want some children to animate but others not?
Omit the variants prop from children you want to keep static. Or give them a variant name that doesn't exist in your variant object—they'll render without animation.