08 Stagger and hierarchy 08 Stagger and hierarchy 08 Stagger and hierarchy 08 Stagger and hierarchy
What moves first is seen first. A stagger sets the reading order, and a small one is enough.
When several things arrive at once, the eye has to decide where to look first. Motion can decide for it. Start the same move on each item a moment after the one before, and the eye follows the wave: first, second, third. That offset is a stagger. It is the overlap from chapter 7, applied to a whole list.
A stagger is an instruction: read this, then this, then this. It also ranks things, because what moves first is seen first. Get the order right and a list reads as easily as a sentence. Make the gaps too long and people sit and wait for your list to finish arriving.
Five rows, twice
Five rows arrive together. Under them is their exposure sheet, the animator’s timing chart: one line per track, time running across in frames. Each bar is one move, from the moment it starts to the moment it lands. Every row has two moves: it rises into place, and it fades in.
Now the same five rows with one change. Each row starts a moment after the one above it.
Look at the sheets first. In figure 8.1 the bars stand in one column: everything starts on the same frame. In figure 8.2 they form a staircase, and the stage follows it. The first list lands as a single block, and your eye has to pick a place to start. The second has a direction. It starts at the top and runs down, and your eye goes with it, top row first, the way you would read the list anyway.
How long a gap?
Three lists. Only the gap between one row and the next is different. Don’t count. Just watch.
A list loads. Which one leads your eye down it without making you wait?
Small steps
Now the numbers. In the test above, every row took 280ms. Only the step between starts changed: 0ms, 30ms and 150ms.
A 60Hz screen draws a new frame every 16.7ms, so a 30ms step is about two frames. That’s too short to see five separate starts. The eye sees one motion with a direction, running down the list. At 150ms the step is nine frames, and each row has almost landed before the next one begins. Now there are five events, and you wait through each of them.
Nothing about a row got slower. Each one still takes 280ms. The list got slower: it isn’t finished until its last row lands, at 400ms with the small step and at 880ms with the large one. That extra time is all in the steps.
| Step | Frames at 60Hz | Reads as |
|---|---|---|
| 0ms | 0 | one block |
| 20–40ms | 1.2–2.4 | one gesture, with a direction |
| 60–80ms | 3.6–4.8 | a visible cascade |
| 150ms and up | 9 and up | a wait |
These ranges are a taste, not a rule; they come from where the eye stops seeing separate starts. For most lists, keep the step between 20 and 40ms. Figure 8.2 uses 40ms, about two and a half frames. Many small things, like the cells of a grid, sit at the low end. A few large ones, like cards, can take the high end.
The count multiplies it
With n items, the last one starts (n − 1) steps after the first. The step is small, but it’s multiplied by the number of items, and the list is only done when its last item lands. With a 280ms move:
| Items | 30ms step: last one lands | 80ms step: last one lands |
|---|---|---|
| 5 | 400ms | 600ms |
| 10 | 550ms | 1000ms |
| 20 | 850ms | 1800ms |
| 50 | 1750ms | 4200ms |
A step that feels quick on five items makes you wait on fifty. So put a limit on it. There are two ways.
- Cap the index. Stagger only the first six to eight items. The rest start together with the last staggered one. By then the order is set, and rows further down are often off screen, where nobody sees their order anyway. Capped at the eighth row, a 30ms step never adds more than 210ms, however long the list.
- Give the cascade a budget. Decide how long the whole stagger may take, and divide it by the count. A long list gets a smaller step and sweeps in, in the same time as a short one.
Who leads
A stagger ranks things. The first to move is the first to be seen, so it had better be the one that matters most. Everything after it reads as following.
- A container leads its contents. The panel arrives, then what is in it. Contents that arrive before their container have nowhere to land.
- A heading leads its body. The title, then the lines under it.
- The news gets its own move. When one thing changes, it moves alone and settles last, while the context moves as a block.
Here a new row arrives at the top of a list.
The old rows are context. They move down together, in one block, to make room, and they don’t stagger: a stagger would turn each of them into something to read. The new row gets a move of its own. It starts as the gap opens and settles last, so it’s where the eye comes to rest.
That’s the other thing a stagger says: these are separate items. Five rows with a stagger are five things. The same five in unison are one thing. Stagger what should be read one by one, and move context as a block. And if everything on a page staggers, nothing leads.
Other orders
Top to bottom is the default because it’s how a list is read. It isn’t the only order.
- Where reading starts. Across a row, begin where the reader begins: the left in English, the right in Arabic or Hebrew.
- Centre out. Start from the item the user touched and spread to its neighbours, like a ripple. The motion begins where attention already is.
- Last first. When the eye is already at the end, start there. A list pinned to the bottom of the screen can run upward.
- Random. No order at all, so nothing leads. It reads as sparkle, not as a list, and is rarely right in an interface.
Find where it breaks
This is the list from figure 8.2 in the Exposure sheet. The Step slider re-spaces every row at once.
- Drag Step down to 0. The staircase collapses into a column, and the list lands as a block.
- Take it past 80ms and watch the bottom row. You start waiting for it.
- Find the largest step that still feels like one gesture to you.
- Set Order to centre out. The middle row leads, and the motion spreads both ways.
- Start from Cascade: six cards on a spring, rippling out from one of them.
The notation
A stagger is nothing but a delay: each item waits its index times the step. In CSS that’s animation-delay, and each item has to know its index. Write it into the markup as a custom property, style="--i: 2", and multiply: calc(var(--i) * 30ms).
Or let the browser count. sibling-index() gives an element’s position among its siblings, starting at 1, and sibling-count() gives how many there are. Both are recent additions, so check current support before relying on them, and put a fallback line above. A browser that doesn’t understand a declaration drops it and keeps the one before. That only works if the new line has no var() in it: a declaration with a var() is accepted when the stylesheet is read and can only fail later, once the variable is filled in. Then it falls back to the property’s initial value, not to the line above.
Two details matter in every version. Give the animation a fill of both, so each item holds its first frame while it waits for its turn; without it, a row sits there fully visible, then vanishes and fades back in. And keep the step small and limited, with a cap or a budget.
/* Each row carries its index in the markup: <li style="--i: 3"> */
.list li {
animation: rise 280ms cubic-bezier(.2, .8, .2, 1) both;
/* 30ms per row. --i counts from 0, so min(..., 7) stops the delay
growing at the eighth row: every later row starts with it. */
animation-delay: calc(min(var(--i, 0), 7) * 30ms);
}
@keyframes rise {
from { opacity: 0; transform: translateY(12px); }
} /* sibling-index() is 1 for the first child. It is new: a browser
that doesn't know it drops that line and keeps the one above
(no --i in the markup means no stagger, just a plain rise). */
.list li {
animation: rise 280ms cubic-bezier(.2, .8, .2, 1) both;
animation-delay: calc(min(var(--i, 0), 7) * 30ms);
animation-delay: calc(min(sibling-index() - 1, 7) * 30ms);
} const rows = [...document.querySelectorAll('.list li')];
// 30ms between starts, but the whole cascade never takes
// more than 240ms (a budget: about 8 rows at 30ms).
// A longer list gets a smaller step: 240 divided by the gaps between rows.
const step = Math.min(30, 240 / Math.max(1, rows.length - 1));
rows.forEach((row, i) => {
row.animate(
[
{ opacity: 0, transform: 'translateY(12px)' },
{ opacity: 1, transform: 'none' },
],
{
duration: 280,
delay: i * step, // ms: row i waits i steps
easing: 'cubic-bezier(.2, .8, .2, 1)',
fill: 'both', // hold the first frame while waiting
},
);
}); import { animate, stagger } from 'motion';
// Motion counts in seconds: 0.03 is 30ms between starts.
animate(
'.list li',
{ opacity: [0, 1], y: [12, 0] },
{ duration: 0.28, ease: [0.2, 0.8, 0.2, 1], delay: stagger(0.03) },
);
// Other orders: from 'first' (the default), 'last', 'center', or an index.
animate(
'.toolbar button',
{ opacity: [0, 1], scale: [0.9, 1] },
{ duration: 0.2, delay: stagger(0.03, { from: 'center' }) },
); A stagger orders things that arrive together. It says nothing about how one thing should arrive or leave, or where from. That is chapter 9.
Judge the gap
Two versions of the same list. Pick the better one, then name why in one word. Watch the bottom row: is it part of the gesture, or are you waiting for it?
Two versions. Which is better? Then say why, in one word.Taste: judging, then naming the reason.
5 trials. Judge with your eyes first; the numbers come after. Three right in a row makes trials harder, a miss eases them.