Shots in this scene
Build: The Product Grid Reveal
In one line
The six accessory cards arrive one after another with Lumen's ease-out token and a stagger capped at 300 ms. The grid still shows at once with no JavaScript and with reduced motion.
Before you start
You will learn
- Add the
--stagger-steptoken and the reveal block tocomponents.css - Add the reveal block to
main.js: number the cards, then reveal when the grid is seen - Check the grid with JavaScript off and with reduced motion
You write code in
In your lumen-motion project (npm run dev): change src/styles/tokens.css, src/styles/components.css and src/js/main.js. index.html does not change. Keep the Phase 1, 3 and 4 blocks in both files.
Files you touch
- src/styles/tokens.css
- src/styles/components.css
- src/js/main.js
Think of it like this
A shop assistant lays six items on the counter one by one, a beat apart. If the assistant is away, the items are simply on the counter already.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- reveal
- Showing content with an entrance animation when it first comes into view.
- IntersectionObserver
- A browser object that tells your code when an element enters or leaves the visible part of the page.
- progressive enhancement
- Building the page so it works without the extra feature, then adding the feature on top for browsers that can use it.
- transitionend
- The event a transition fires when it finishes, once per property.
Step by step
01Add the token
In tokens.css add the marked Phase 5 block with --stagger-step: 60ms; inside :root.
02Add the CSS block
At the end of components.css paste the Phase 5 block. Check that nothing hides .card outside .grid.reveal.
03Add the JavaScript block
At the end of main.js paste the Phase 5 block. Scroll the page: the cards arrive one after another.
04Break the page on purpose
In DevTools, disable JavaScript and reload: the grid is there at once. Turn on reduced motion in the Rendering tab and reload: also at once, with no movement.
Try it yourself
- 1
Watch the wave
In "The reveal and its fallbacks" press Replay, pause and step. Measured 150 ms after Replay: card 1 is at opacity 0.99, card 3 at 0.52 (6.8 px of 14 still to rise) and card 6 has not started (it starts at 300 ms). Everything is at 1 by 520 ms.
- 2
Switch the system setting
Switch on Reduced motion in the timeline. The six cards show with opacity 1 and no transform at the first frame, and no tracks exist.
Code & diagrams
Six cards revealed by the Phase 5 CSS and JavaScript. Replay runs the wave again. "Pretend: no JavaScript" removes the .reveal class, which is the state of a page whose script never ran. Switch on Reduced motion in the timeline and the cards are there from the start.
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
reveal class: true · in: false
--i: 0 1 2 3 4 5The page as it stands after this phase: index.html's body, tokens.css, both stylesheets, toast.js and main.js inlined (local imports do not run in the preview). Scroll to Accessories.
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
cards: 6 · --i: 0 1 2 3 4 5
hidden until revealed: trueYour project files
Your lumen-motion project after this topic. Click a file to see it in full.
The whole file after this topic. If yours differs, you can copy this one and carry on.
/* tokens.css: Lumen's motion tokens. One place for durations and easings, so every component speaks the same language.
Phase 2 starts this file; later phases add springs and delays. Never rename a token: components depend on the names. */
:root {
/* durations (the Nielsen Norman Group ladder, Topic 2.4) */
--dur-fast: 120ms; /* feedback: a press, a hover colour */
--dur-base: 220ms; /* most things: toasts, panels, dialogs entering */
--dur-slow: 400ms; /* large movement: a drawer across the screen */
/* easings (Topic 5.3 explains the choices) */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* things arriving */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* things leaving */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* things moving between two places on screen */
/* ── Phase 5: choreography ── */
--stagger-step: 60ms; /* gap between neighbours in a reveal; main.js caps how many steps count */
/* ── end Phase 5 ── */
}
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
Hide the cards in plain CSS
In the CSS replace .grid.reveal .card { with .card { (so the hiding rule no longer depends on the class).
Break #2
Forget to drop the delay
Delete the line .grid.reveal.in.done .card { transition-delay: 0s; } from the CSS, wait for the reveal to end, then read the sixth card's transition-delay.
Myth vs fact
Myth
Hide the cards in CSS and let JavaScript show them.
Fact
Then the page depends on the script. Hide only under a class the script adds.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The observer fires at 10% visible, and
disconnect()makes the reveal run once: scrolling back never replays it.
Remember this
- 1
The problem this solves (Ama's and Mei's problem): the six accessory cards appear in one frame and the page feels abrupt. Mei's storyboard: each card 60 ms after the last, 300 ms of stagger at most, with the Phase 2 tokens.
- 2
What it adds, and how it plugs in.
tokens.cssgets--stagger-step: 60ms.components.cssgets a marked Phase 5 block: cards are hidden only under.grid.reveal, shown with.in, and use--dur-base,--ease-outandtransition-delay: calc(var(--i, 0) * var(--stagger-step)).main.jsnumbers the cards withMath.min(i, 5), then adds.reveal, and anIntersectionObserveradds.inwhen the grid scrolls into view. - 3
Why the class comes from JavaScript. If the hiding rule applied to every
.card, a visitor whose script failed to load, or who has an old browser, would see an empty grid for ever. Here.revealis added last, after the observer is watching. Without JavaScript, withoutIntersectionObserver, or with reduced motion (checked withmatchMedia, and again in a CSS media query), the class is never added and the cards just show. - 4
Clean-up after the show. The delay stays on each card while
.inis set, so any later transition on a card (a hover, in a later phase) would wait too. When the last card'stransitionendfires,main.jsadds.done, and.donesets the delay back to 0. - 5
What the launch site can do now that it could not before: the accessories arrive in a calm, ordered wave (the last card is done after 520 ms), still in one frame for anyone who needs it. What is still missing: the toggle that lets a visitor override the system setting (Phase 7), the cart bump (Phase 8) and the gallery filter motion (Phase 10). If the visitor changes the system setting while the page is open, the reveal does not react; Phase 7 adds that.
Explain it without notes
Why is .reveal added after the observer is created?
What happens to the grid with JavaScript off?
Practice
Make the reveal start from the last card instead of the first.
Change the stagger so it totals at most 240 ms with the 60 ms step.
Done when you can
The cards arrive one after another when you scroll to them
With JavaScript off the grid shows at once
With reduced motion the grid shows at once with no movement
The stagger never exceeds 300 ms