Advanced: Coordinating Multiple Layout Animations
Coordinating multiple layout animations across dozens of elements requires careful orchestration. Without a system, animations become chaotic—elements move at different speeds, some complete early, and the overall effect looks disjointed. Framer Motion's variant system provides a declarative way to coordinate motion across nested components.
This article covers advanced patterns: parent-child variant chains, conditional staggering, timing orchestration, and layered animations. These techniques enable professional-grade UI motion in large, complex applications.
Variant System for Orchestration
Variants are named animation states that propagate from parent to child components. When a parent animates to a new variant, all children with matching variants animate together, controlled by parent-level timing rules like staggerChildren.
Here is a dashboard that animates 12 cards entering with staggered motion:
import { motion } from 'framer-motion';
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: 'easeOut' },
},
};
function DashboardCard({ title }) {
return (
<motion.div
variants={cardVariants}
style={{
padding: '20px',
backgroundColor: '#f0f0f0',
borderRadius: '8px',
minHeight: '150px',
}}
>
<h3>{title}</h3>
<p>Card content goes here.</p>
</motion.div>
);
}
export default function Dashboard() {
const cards = [
'Revenue',
'Users',
'Retention',
'Churn',
'ARPU',
'LTV',
'CAC',
'Sessions',
'Bounce Rate',
'Time on Site',
'Conversion',
'AOV',
];
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '20px',
padding: '20px',
}}
>
{cards.map((card) => (
<DashboardCard key={card} title={card} />
))}
</motion.div>
);
}
The container animates to visible, which triggers all children to animate with a 0.1 s stagger between each. The first child animates after a 0.2 s delay. This creates a cascading entrance effect without explicit per-child timing code.
Hierarchical Staggering
For nested hierarchies (sections with cards, cards with items), use staggering at each level:
const sectionVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
},
},
};
const cardVariants = {
hidden: { opacity: 0, x: -20 },
visible: {
opacity: 1,
x: 0,
transition: {
staggerChildren: 0.05,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0 },
};
function Section({ title, cards }) {
return (
<motion.div
variants={sectionVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
style={{ marginBottom: '30px' }}
>
<h2>{title}</h2>
<motion.div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '15px',
}}
>
{cards.map((card) => (
<motion.div
key={card.id}
variants={cardVariants}
style={{
padding: '15px',
backgroundColor: '#f5f5f5',
borderRadius: '6px',
}}
>
<h4>{card.title}</h4>
{card.items.map((item, i) => (
<motion.div key={i} variants={itemVariants}>
{item}
</motion.div>
))}
</motion.div>
))}
</motion.div>
</motion.div>
);
}
Sections stagger at 0.15 s intervals. Within each section, cards stagger at 0.05 s. This creates a waterfall entrance: sections appear, then cards within each section cascade.
Conditional Timing
Vary timing based on user interactions or application state:
const listVariants = {
hidden: { opacity: 0 },
visible: (isExpanded) => ({
opacity: 1,
transition: {
staggerChildren: isExpanded ? 0.05 : 0.15,
staggerDirection: isExpanded ? 1 : -1,
},
}),
};
export function CollapsibleList({ isExpanded, items }) {
return (
<motion.ul
custom={isExpanded}
variants={listVariants}
animate={isExpanded ? 'visible' : 'hidden'}
style={{ listStyle: 'none' }}
>
{items.map((item) => (
<motion.li
key={item.id}
variants={{
hidden: { opacity: 0, height: 0 },
visible: { opacity: 1, height: 'auto' },
}}
>
{item.name}
</motion.li>
))}
</motion.ul>
);
}
The custom prop passes isExpanded to variants. When expanded, items stagger at 0.05 s (fast cascade). When collapsed, they stagger in reverse at 0.15 s (slow exit). The staggerDirection: -1 reverses the order on exit.
Orchestrating Fade, Scale, and Rotate
Complex motions combine multiple property animations. Use variants to keep timing consistent:
const modalVariants = {
hidden: { opacity: 0, scale: 0.9, rotate: -5 },
visible: {
opacity: 1,
scale: 1,
rotate: 0,
transition: {
opacity: { duration: 0.2 },
scale: { duration: 0.3, delay: 0.1 },
rotate: { duration: 0.3, delay: 0.1 },
},
},
exit: {
opacity: 0,
scale: 0.9,
rotate: 5,
transition: {
opacity: { duration: 0.2 },
scale: { duration: 0.3 },
rotate: { duration: 0.3 },
},
},
};
function Modal() {
return (
<motion.div
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
>
Modal content
</motion.div>
);
}
The modal fades in immediately, then scales and rotates with a 0.1 s delay. On exit, all properties animate simultaneously at the same speed.
Drag-Driven Coordination
When elements respond to drag input (scroll, swipe), coordinate all child animations to a single parent drag gesture:
import { motion, useMotionValue, useTransform } from 'framer-motion';
export function CarouselWithDrag({ items }) {
const x = useMotionValue(0);
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
const scale = useTransform(x, [-200, 0, 200], [0.8, 1, 0.8]);
return (
<motion.div
drag="x"
dragElastic={0.2}
onDrag={(e, info) => {
// Manually update if needed
}}
style={{ x }}
>
{items.map((item, i) => (
<motion.div
key={i}
style={{
opacity: i === 0 ? opacity : 1,
scale: i === 0 ? scale : 1,
}}
>
{item.name}
</motion.div>
))}
</motion.div>
);
}
The parent's x motion value drives child opacity and scale. As the user drags, children respond in real-time without explicit per-child handlers.
Gesture-Triggered Orchestration
Combine hover, tap, and focus states to orchestrate complex interactions:
const containerVariants = {
rest: {
transition: {
staggerChildren: 0.07,
staggerDirection: -1,
},
},
hover: {
transition: {
staggerChildren: 0.07,
delayChildren: 0.2,
},
},
};
const itemVariants = {
rest: { opacity: 0.6, scale: 0.95 },
hover: { opacity: 1, scale: 1 },
};
export function CardGrid({ cards }) {
return (
<motion.div
variants={containerVariants}
initial="rest"
whileHover="hover"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '15px',
}}
>
{cards.map((card) => (
<motion.div
key={card.id}
variants={itemVariants}
whileHover={{ scale: 1.05 }}
>
{card.title}
</motion.div>
))}
</motion.div>
);
}
Hovering the grid animates all cards to full opacity and scale with a stagger. Individual card hover adds a slight extra scale for layered feedback.
Performance: Reducing Animation Overhead
Large coordinated animations can overwhelm the browser. Optimize:
- Limit active animations: Animate at most 20–30 elements simultaneously. For larger lists, use virtualization.
- Use GPU-safe properties: Animate only
transformandopacity. Avoidwidth,height,top,left. - Increase duration slightly: 400–500 ms animations spread measurement cost over more frames (16 ms × 30 frames = 480 ms).
- Lazy load content: Fetch detail content after the entrance animation completes.
const containerVariants = {
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
duration: 0.4, // Longer duration = more forgiving on CPU
},
},
};
Key Takeaways
- Variants propagate from parent to child, enabling coordinated multi-element animation.
staggerChildrensequences child animations;delayChildrenadds initial delay.- Hierarchical staggering (section → card → item) creates layered entrance effects.
- Conditional timing (based on
customprop) adapts animation to application state. - Limit to 20–30 simultaneous animations; use virtualization for large lists.
Frequently Asked Questions
Can I animate elements in reverse order on exit?
Yes. Use staggerDirection: -1 in the exit variant to reverse stagger order. Combined with transition.staggerChildren, this creates polished cascade-out effects.
How do I synchronize animations across sibling containers?
Use a shared parent variant and pass timing via the parent. All descendants inherit the parent's staggerChildren rules automatically.
Can I pause and resume coordinated animations?
Framer Motion doesn't expose pause/resume. Workaround: toggle animate prop to switch variants and resume animations.
What is the maximum number of elements I can stagger smoothly?
On modern devices (4-core CPU), 30–50 elements stagger smoothly at 60 FPS. Test on real low-end devices (2-core, 4GB RAM). Use virtualization for 100+.
Can I nest Reorder.Group inside coordinated animations?
Yes. Each component layer animates independently. Reorder handles its own layout; parent variants handle the container. No conflict if you use distinct layoutId and variant values.