Shots in this scene
Stagger and Choreography
In one line
A stagger starts the members of a group one after another, each a little later than the last. Give each item its number in a custom property, multiply it by a step, and cap the number so a long list never makes anyone wait.
Think of it like this
A row of dominoes. If all fall in the same instant you see a flat slab drop. If each hits the next, your eye follows the fall from one end to the other, and the whole group reads as one event with a direction. A stagger is the dominoes, with the delay chosen by you.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- stagger
- Starting the items of a group at slightly different times, so they arrive one after another.
- custom property
- A CSS variable you name yourself, such as
--i. You can set it in a stylesheet, in HTML or with JavaScript, and read it withvar(--i). - cap
- An upper limit. Here, the largest number allowed in
--i, so the delay stops growing. - budget
- The total time a whole reveal may take, however many items it has.
- origin
- The item where the wave starts: the first, the last, the centre, an edge or a corner.
Step by step
01Reveal six chips
Run "One property per chip" and press Reveal. Pause and step. After 100 ms the first chip is about half visible. At 200 ms the third is at 31%, and the sixth has not started: its delay is 350 ms. At 350 ms the third is at 87%. At 650 ms all six are done.
02Count and cap
Run "1, 6 and 100 items". The console prints each case. One item has no delay at all. Six items end at 300 ms. A hundred items end at 300 ms too, with only 6 different delays: the last squares pop in together.
03Look at the late batch
Run "Items that arrive later". The first batch is numbered 0, 1, 2. Press Add 3 more: the new three are also 0, 1, 2, so their delays are 0s, 0.07s and 0.14s, not 0.21s onwards.
04Try the origins
In the stagger board choose center, then grid-corner, and press Play. Each card shows its delay. Drag the each slider to change the step. Notice that in this 3-by-2 grid, from the corner, the card to the right of the corner and the card below it share one delay: both are one step away.
Try it yourself
- 1
Remove the cap
In "1, 6 and 100 items" change
Math.min(i, CAP)toiand press 100. The console now prints100 items: last delay 5940 ms (uncapped 5940 ms), distinct delays 100. The last square waits almost six seconds. - 2
Number by the whole list
In "Items that arrive later" change
chip.style.setProperty("--i", i);tochip.style.setProperty("--i", row.children.length);. The second batch now prints delays of0.21s 0.28s 0.35s: the new items wait for time that has already passed.
Code & diagrams
Try it: interactive
The stagger board
Choose a wave origin and watch each card's delay. Drag the step slider, and read the GSAP options and the CSS the board prints.
Six chips, each with its number in --i, and one CSS rule that multiplies it by 70 ms. Press Reveal, then pause and step. The console prints the numbers at load.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
--i values: 0 1 2 3 4 5The same reveal for different counts, with a step of 60 ms and a cap of 5. The console prints the delay of the last item and how many different delays exist. Press the three buttons.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
0 items: nothing to number, nothing to wait for
1 items: last delay 0 ms (uncapped 0 ms), distinct delays 1
6 items: last delay 300 ms (uncapped 300 ms), distinct delays 6
100 items: last delay 300 ms (uncapped 5940 ms), distinct delays 6Each batch is numbered from 0. The console prints the delays of the newest batch: the page adds one batch at load and one more by clicking the button for you.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
batch of 3, page now has 3 items, delays: 0s 0.07s 0.14s
batch of 3, page now has 6 items, delays: 0s 0.07s 0.14sBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to set --i
In "One property per chip" delete the line chip.style.setProperty("--i", i);, press Reveal.
Break #2
Cap of zero
In "1, 6 and 100 items" set const CAP = 0; and press the buttons.
Myth vs fact
Myth
A longer stagger always looks more elegant.
Fact
At some size of step the eye stops seeing a wave and sees separate events, and a long total means users wait for the page. Keep the whole wave short: Lumen's is 300 ms of stagger.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A staggered reveal makes the page wait for its own animation: nothing in the group is at full opacity until its delay passes. Content that matters (a price, the reserve button) should never sit behind a long stagger.
- ▸
animation-fill-mode: both(orbackwards) matters here: during an item's delay it must already show thefromstate, or the item flashes visible first, then jumps to hidden when its animation starts (Topic 4.3). The examples useboth.
Remember this
- 1
The problem this solves: six cards that appear in one frame look abrupt, and the eye does not know where to look first. Six cards that arrive together with identical motion look like one grey block. Starting them in turn gives order and direction, without any item moving more than a few pixels.
- 2
One custom property per item. Number the items in the page:
style="--i: 2"or, in JavaScript,item.style.setProperty("--i", i). In CSS,transition-delay: calc(var(--i) * 70ms)turns the number into a delay. The CSS stays one rule for all items, and the number is data. For six chips the last waits 350 ms and the reveal ends at 650 ms ((n − 1) × step + duration). - 3
Cap it. The total time grows with the list: 100 items at 60 ms means the last item starts after 5,940 ms. Nobody waits six seconds to see a list. Cap the number:
Math.min(i, 5)means items from the sixth onwards share the largest delay. The second example shows 1, 6 and 100 items: the last delay is 0, 300 and 300 ms. - 4
A budget instead of a step. The other way is to fix the total and divide it:
step = min(60, budget / (n − 1)). Every item then has its own delay, and 100 items with a 300 ms budget give 3 ms steps (below one frame at 60 Hz, so several share a frame). GSAP'sstaggerhas both ideas:eachis the step,amountis the total budget (anamountof 0.5 starts the last item at 0.5 s, however many items there are). - 5
Direction and origin. The wave can start at the first item, the last, the centre or the edges, or radiate from a corner of a grid. With a custom property you compute the number in JavaScript:
Math.abs(i - (n - 1) / 2)for the centre, orMath.hypot(col, row)for the distance from a corner. The widget shows each origin and the CSS and GSAP forms that produce it. - 6
Which order is the DOM order?
--ifollows the order in the HTML. In a right-to-left page the first item is at the right, so the wave runs right to left, like reading. Butflex-direction: row-reverseor the CSSorderproperty change what is on screen without changing the number, so the wave can run the wrong way. - 7
Items added later. A list that grows (search results, a chat) must number each new batch from 0. If you number by position in the whole list, the new items wait for the old ones' time: a second batch of three with the total index waits 210, 280 and 350 ms. The third example prints the delays of both batches, and a reveal like this is only for the first arrival, not for every later item.
Explain it without notes
How do you stagger six items with one CSS rule?
Why must a stagger be capped, and what are the two ways to cap it?
Why do items added later need their own numbering?
Practice
A search results list shows 8 items per page. Give the CSS and JavaScript for a stagger with a 40 ms step and a 200 ms total budget.
Why should the wave not run for every item added to a growing list?
Done when you can
You can stagger a group with
--iandcalc()You can cap the delay by index or by total budget
You can name a risk of an unset
--iand of late itemsYou can pick an origin and say how the DOM order affects it