Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 5.5Intermediate ~22 min · shot 5 of 6

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.

0/6 · 0%

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 with var(--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. 1

    Remove the cap

    In "1, 6 and 100 items" change Math.min(i, CAP) to i and press 100. The console now prints 100 items: last delay 5940 ms (uncapped 5940 ms), distinct delays 100. The last square waits almost six seconds.

  2. 2

    Number by the whole list

    In "Items that arrive later" change chip.style.setProperty("--i", i); to chip.style.setProperty("--i", row.children.length);. The second batch now prints delays of 0.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.

One property per chip

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.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

--i values: 0 1 2 3 4 5
1, 6 and 100 items

The 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.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

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 6
Items that arrive later

Each 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.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

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.14s

Break 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.

terminal
$ Press Reveal and read the console
── what you'll see ──
--i values: (six empty values), and after Reveal the last chip's transition-delay is 0s. No track appears: the six chips pop in together in one frame.
Measured: at 150 ms after Reveal every chip is already at opacity 1 and the timeline shows no animations.

Break #2

Cap of zero

In "1, 6 and 100 items" set const CAP = 0; and press the buttons.

terminal
$ Press 6 and read the console
── what you'll see ──
6 items: last delay 0 ms (uncapped 300 ms), distinct delays 1
Measured: every item has --i 0, so all start together.

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 (or backwards) matters here: during an item's delay it must already show the from state, or the item flashes visible first, then jumps to hidden when its animation starts (Topic 4.3). The examples use both.

Remember this

  1. 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. 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. 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. 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's stagger has both ideas: each is the step, amount is the total budget (an amount of 0.5 starts the last item at 0.5 s, however many items there are).

  5. 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, or Math.hypot(col, row) for the distance from a corner. The widget shows each origin and the CSS and GSAP forms that produce it.

  6. 6

    Which order is the DOM order? --i follows 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. But flex-direction: row-reverse or the CSS order property change what is on screen without changing the number, so the wave can run the wrong way.

  7. 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

01

How do you stagger six items with one CSS rule?

02

Why must a stagger be capped, and what are the two ways to cap it?

03

Why do items added later need their own numbering?

Practice

01

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.

02

Why should the wave not run for every item added to a growing list?

Done when you can

  • You can stagger a group with --i and calc()

  • You can cap the delay by index or by total budget

  • You can name a risk of an unset --i and of late items

  • You can pick an origin and say how the DOM order affects it