| name | timeline-choreographer |
| description | This skill should be used when the user needs to orchestrate multiple elements animating in sequence or with complex timing relationships. Trigger when the user mentions "stagger animations", "animation sequence", "choreograph animations", "elements animate one after another", "staggered reveal", "delay between animations", "GSAP timeline", "Framer Motion variants", "animation timing", "orchestrate", "50ms stagger", or has a component where multiple elements need to animate in a specific order. Also trigger when the user says an animation "looks like everything explodes at once" or wants a more "coordinated" or "sequential" entrance. |
Timeline Choreographer
When multiple elements animate together, the difference between "everything flashed at once" and "beautifully sequenced" is choreography: the deliberate arrangement of when each element moves, for how long, and in what relationship to others.
Core principle: The eye follows the first thing that moves. Everything after should feel like a logical consequence.
The Three Choreography Rules
- Lead with the most important element — primary content moves first. Secondary and decorative elements follow.
- Stagger related siblings by 40–70ms — creates a readable wave. Below 30ms: imperceptible (might as well be simultaneous). Above 80ms: feels slow and disconnected.
- Maximum 2–3 focal points per sequence — every additional concurrent animated element costs cognitive attention.
Framer Motion Variants — The Declarative Approach
Variants propagate from parent to children automatically. The parent's staggerChildren delays each child's entry sequentially.
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.06,
delayChildren: 0.1,
when: "beforeChildren",
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 16, filter: "blur(4px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { duration: 0.3, ease: [0, 0, 0.58, 1] },
},
};
function StaggeredContent({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item, i) => (
<motion.li key={i} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}
Dashboard grid stagger (diagonal pattern):
const gridVariants = {
visible: {
transition: {
staggerChildren: 0.04,
delayChildren: 0.05,
},
},
};
const cardVariants = {
hidden: { opacity: 0, scale: 0.96 },
visible: (i) => ({
opacity: 1,
scale: 1,
transition: {
delay: (Math.floor(i / cols) + (i % cols)) * 0.04,
duration: 0.28,
},
}),
};
GSAP Timeline — The Imperative Approach
Use GSAP when:
- You need precise overlap control (not just stagger)
- Mix of CSS, SVG, and DOM element animations
- Complex orchestration with conditional branches
- Scroll-linked choreography (with ScrollTrigger)
import gsap from "gsap";
const tl = gsap.timeline({
defaults: { ease: "power2.out" },
delay: 0.1,
});
tl.from(".hero-eyebrow", { y: 20, opacity: 0, duration: 0.4 })
.from(".hero-headline", { y: 30, opacity: 0, duration: 0.5 }, "-=0.2")
.from(".hero-subhead", { y: 20, opacity: 0, duration: 0.4 }, "-=0.2")
.from(".hero-cta", { y: 15, opacity: 0, duration: 0.35 }, "-=0.15")
.from(".hero-image", { scale: 0.95, opacity: 0, duration: 0.6 }, "<");
Stagger with GSAP:
gsap.from(".card", {
opacity: 0,
y: 20,
stagger: {
each: 0.06,
from: "start",
ease: "power2.in"
},
duration: 0.3,
ease: "power2.out",
scrollTrigger: ".card-grid",
});
Stagger Timing Reference
| Stagger interval | Effect |
|---|
| < 30ms | Effectively simultaneous — imperceptible |
| 40–60ms | Smooth wave — most common, natural |
| 60–80ms | Readable cascade, each element distinct |
| 80–120ms | Deliberate, each element gets full attention |
| > 120ms | Slow — use only for < 4 items |
Longer stagger = more deliberate = higher hierarchy. Use generous stagger for onboarding, hero, and empty states. Use tight stagger for lists, grids, and content sections.
Hero Sequence Timing Template
Standard pattern for a 5-element hero section at feel-premium speed:
t=0ms: Eyebrow / label (if present) — fade + translateY(16px), 350ms
t=150ms: Primary headline — fade + translateY(16px), 450ms
t=280ms: Subheadline — fade + translateY(12px), 380ms
t=380ms: CTA button(s) — fade + scale(0.97→1), 300ms
t=280ms: Hero image (concurrent with subheadline) — fade + scale(0.97→1), 500ms
Total sequence: ~780ms from first element to fully rendered — short enough to not feel slow, long enough to feel crafted.
Exit Choreography
Exits are the reverse of entrances — but should run in reverse order and at ~70–80% of entrance duration. The last thing to enter is the first to leave.
const exitVariants = {
visible: { opacity: 1, y: 0 },
hidden: {
opacity: 0,
y: -8,
transition: { duration: 0.2, ease: [0.4, 0, 1, 1] }
},
};
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="content"
initial="hidden"
animate="visible"
exit="hidden"
variants={containerVariants}
>
{children}
</motion.div>
)}
</AnimatePresence>
Common Mistakes
| Mistake | Consequence | Fix |
|---|
| Everything at t=0 | Visual explosion, no narrative | Add staggerChildren or explicit delays |
| Stagger > 100ms for 8+ items | Sequence too slow to watch | Cap at 60ms for large sets |
| Different easing per element | Incoherent feel | Use a shared defaults in GSAP or shared variant config |
| Entrance without exit | Spatial confusion on unmount | Always pair AnimatePresence + exit variants |
| Mixing simultaneous and staggered randomly | No clear read order | Define the visual hierarchy first, then assign delays accordingly |
Additional Resources
references/choreography-patterns.md — Page transition choreography, tab switch sequences, modal open/close, loading → content reveal transitions, and a full library of named stagger patterns (cascade, wave, from-center, diagonal, random)