Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 5.6Intermediate ~36 min · shot 6 of 6

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.

0/6 · 0%

Before you start

You will learn

  • Add the --stagger-step token and the reveal block to components.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. 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. 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

The reveal and its fallbacks

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.

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

reveal class: true · in: false
--i: 0 1 2 3 4 5
The launch page, with the reveal

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

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

cards: 6 · --i: 0 1 2 3 4 5
hidden until revealed: true

Your project files

Your lumen-motion project after this topic. Click a file to see it in full.

src/styles/tokens.csscss

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

terminal
$ Press "Pretend: no JavaScript" and look at the cards
── what you'll see ──
The cards are still invisible: toggling .reveal changes nothing, because the hiding no longer depends on it. With a failed script the grid would stay empty.

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.

terminal
$ getComputedStyle(card).transitionDelay on the sixth card, 1.5 s after load
── what you'll see ──
0.3s, with the grid's class list "grid reveal in done". With the line it reads 0s.
Measured in real time.

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

    What it adds, and how it plugs in. tokens.css gets --stagger-step: 60ms. components.css gets a marked Phase 5 block: cards are hidden only under .grid.reveal, shown with .in, and use --dur-base, --ease-out and transition-delay: calc(var(--i, 0) * var(--stagger-step)). main.js numbers the cards with Math.min(i, 5), then adds .reveal, and an IntersectionObserver adds .in when the grid scrolls into view.

  3. 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 .reveal is added last, after the observer is watching. Without JavaScript, without IntersectionObserver, or with reduced motion (checked with matchMedia, and again in a CSS media query), the class is never added and the cards just show.

  4. 4

    Clean-up after the show. The delay stays on each card while .in is set, so any later transition on a card (a hover, in a later phase) would wait too. When the last card's transitionend fires, main.js adds .done, and .done sets the delay back to 0.

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

01

Why is .reveal added after the observer is created?

02

What happens to the grid with JavaScript off?

Practice

01

Make the reveal start from the last card instead of the first.

02

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