Chapter 03 of 11Part 1 · Principles
Staggered animation and sequencing
How to offset a group of animations so they read in order, and how to keep the whole reveal under a second.
9 minBy Zuzo StudioUpdated
In this chapter you will
- explain the difference between stagger, overlap and sequence
- work out the total time of a reveal and keep it within a budget
- stagger the words and buttons of your hero in plain CSS
Before you startThis builds on CSS easing functions and timing.
When a group of things appears at once, the eye does not know where to start. When the same things arrive a few milliseconds apart, it reads them in order. That small offset is a staggered animation: each item in a group starts its animation a fixed delay after the one before. Done well, it guides the reader through a layout. Done badly, it makes them wait for a list to finish arriving. This chapter covers stagger and overlap, how to choose the order, how to keep the total time short, and how to build a stagger in plain CSS. You finish by staggering the words of your hero heading and its two buttons.
01Stagger, overlap and sequence
Three words come up whenever motion designers talk about groups.
- Stagger is a fixed delay between the start of each item in a set of similar things: cards in a grid, items in a menu, words in a heading. Every item has the same animation, only its start time differs.
- Overlap is when the next item starts before the previous one has finished. With a 300ms animation and a 60ms stagger, five items are moving at the same moment. Overlap is what makes a stagger feel like one movement instead of a queue.
- Sequence is the order of different groups: the heading, then the paragraph, then the buttons. A sequence can contain staggers, and the groups in it usually overlap too.
The difference between overlap and a queue is easy to feel. If each item waits for the last to finish, five 300ms items take 1.5s and look like a slideshow. With a 60ms stagger they take 540ms and read as a single wave. This is the "follow through and overlapping action" idea from the first chapter, translated to interfaces.
02Hierarchy and reading order
The order of a staggered animation tells the reader what to look at first, so it should match the order they would read in anyway.
Follow the reading order. In a left to right language that means top to bottom, then left to right. A grid of cards should reveal row by row, not column by column, and certainly not from the bottom up.
Lead with what matters. In a sequence, the most important element arrives first. On a hero that is the heading, then the supporting line, then the call to action. If the buttons land first, the eye goes to them before it knows what they are for.
Match the structure. Staggering only makes sense for items that belong together. A heading and a footer link are not a group, so giving them a shared stagger suggests a relationship that is not there.
The demo below has a list of eight cards that animate in. Change the stagger delay between 0 and 200ms, and the order between start, end, center and random, then press Replay. It shows the total time the reveal takes for each setting.
A stagger starts each item a little after the one before it, so a list reads in order instead of arriving all at once. Small delays of 30 to 80 milliseconds usually read best.
Try each order in turn. Start follows reading order and suits almost everything. End reverses it, which mostly feels wrong unless the items are leaving. Center spreads outwards from the middle, which works when the movement comes from a point, such as a menu opening from a central button. Random looks lively for a second and gives the eye no path at all. We almost never use it for content, only occasionally for decorative shapes.
03The total time budget
The number that matters is not the stagger delay, it is when the last item finishes. For a set of items that all share one animation:
total = (number of items - 1) x stagger + duration
8 cards, 60ms stagger, 300ms animation = 720ms
8 cards, 150ms stagger, 300ms animation = 1350ms
30 rows, 60ms stagger, 300ms animation = 2040ms
The same stagger that feels quick on eight cards makes thirty rows take two seconds. A long list has to finish quickly, because nobody will wait for row 30 to read row 1 again. Two habits keep the total in check.
Set a budget first. Decide how long the whole reveal may take, then work out the stagger from it. For a hero intro we aim for the whole sequence to finish in about a second. For a list that appears after a click, less than half a second.
Cap the delay. Stagger the first few items and let the rest arrive together. In CSS, min() does that in one line: the delay grows with the index until it reaches a ceiling.
.card {
/* items after the sixth all share the sixth item's delay */
animation-delay: calc(min(var(--i), 6) * 60ms);
}
04Building a staggered animation in CSS
CSS has no stagger property. A stagger is just a delay that is different for each item, so all you need is a way to give each item its index and multiply it.
An index custom property
The most reliable method today is to write each item's index as a custom property in the HTML, then use it in animation-delay or transition-delay.
<ul class="cards">
<li style="--i: 0">Strategy</li>
<li style="--i: 1">Design</li>
<li style="--i: 2">Build</li>
</ul>
.cards li {
animation: rise var(--dur-slow) var(--ease-out) backwards;
animation-delay: calc(var(--i) * 60ms);
}
@keyframes rise {
from { transform: translateY(1rem); opacity: 0; }
}
The @keyframes block describes the start of the movement and the backwards fill mode applies that starting state during the delay, so items do not flash in place before they move. We go through keyframes properly in CSS keyframe animation. The same calc() works with transitions, with one catch: a transition delay applies in both directions, so a staggered hover is also staggered when the pointer leaves.
sibling-index()
Newer CSS can read an element's position directly. The sibling-index() function returns the element's position among its siblings, starting from 1 like :nth-child(). MDN marks it as Baseline 2026, newly available, so it works in current browsers but not in older ones still in use. Use it with the custom property as a fallback:
.cards li {
animation-delay: calc(var(--i) * 60ms);
}
@supports (animation-delay: calc(sibling-index() * 1ms)) {
.cards li { animation-delay: calc((sibling-index() - 1) * 60ms); }
}
For longer, conditional or interactive sequences, CSS delays get hard to manage. That is where a timeline library helps, and in GSAP timelines we rebuild this whole intro as one sequence you can pause and reverse.
05Sequencing the hero
Put the pieces together and a hero intro is one sequence of three groups, each overlapping the last:
| Group | Starts at | What it does |
|---|---|---|
| Heading words | 0ms | Rise in, 60ms apart |
| Lede | 200ms | Rises in once |
| Buttons | 350ms | Rise in, 80ms apart |
With the seven words of the project heading and your --dur-slow of 400ms, the last word lands at 760ms and the last button at 830ms. Everything is in place within a second, and at no point is the heading invisible.
:root { --stagger: 60ms; }
.hero-title .word { display: inline-block; }
.hero-title .word, .hero-lede, .hero-actions .button {
animation: rise var(--dur-slow) var(--ease-out) backwards;
animation-delay: calc(var(--start, 0ms) + var(--i) * var(--stagger));
}
.hero-lede { --start: 200ms; }
.hero-actions .button { --start: 350ms; --stagger: 80ms; }
@keyframes rise {
from { transform: translateY(0.4em); }
}
Check your understanding
3 questions
Key takeaways
- A stagger gives each item in a group the same animation with a start time a fixed delay apart.
- Overlap, where the next item starts before the last finishes, makes a stagger feel like one movement.
- The order should follow reading order and lead with the most important element.
- The total time is the number of items minus one, times the stagger, plus the duration, so cap the delay on long lists.
- In CSS a stagger is an index multiplied into animation-delay, from a custom property or sibling-index().
Sources
- animation-delayMDN Web Docs, checked
- transition-delayMDN Web Docs, checked
- sibling-index()MDN Web Docs, checked