Shots in this scene
Build: The Launch Site Storyboard
In one line
Write the motion spec for the whole launch site, and add the six motion tokens in tokens.css: three durations and three easings. Nothing moves yet, but every motion the course builds now has a job, numbers and a calm version.
Before you start
You will learn
- Write docs/motion-spec.md for every motion the site will get
- Add tokens.css with the six motion tokens
- Link tokens.css from index.html
You write code in
In your lumen-motion project: add two files and change one, then run npm run dev
Files you touch
- docs/motion-spec.md
- src/styles/tokens.css
- index.html
Think of it like this
Before a building goes up, the architect draws the plans and the builders agree on a set of standard parts (doors, windows). Here the spec is the plan and the tokens are the standard parts.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- design token
- A named value (here a duration or easing) used everywhere instead of the raw number.
- custom property
- A CSS variable such as
--dur-fast, read withvar(--dur-fast). - :root
- The selector for the page's top element. Variables defined there are available everywhere.
- invalid at computed-value time
- A declaration whose
var()has no value. The browser drops it silently and uses the property's initial value.
Step by step
01Create docs/motion-spec.md
Add the file under "Your project files". Read the table once: every row has a reduced-motion cell. This document will grow in later phases.
02Create src/styles/tokens.css
Add the six tokens in :root. Do not rename them: later phases depend on these names.
03Link it first
In index.html, add the marked Phase 2 block before the other two stylesheets. The order does not break variables, but reading tokens first is a good habit.
04Check it in the browser
With npm run dev, open DevTools and select the html element: the Styles pane lists the six tokens under :root. In the Console, getComputedStyle(document.documentElement).getPropertyValue("--dur-base") gives 220ms.
Try it yourself
- 1
Change one token
In "The six tokens at work", change
--dur-base: 220msto900msand press Run. Click base and fast: the base transition now lasts 900 ms (a 900 ms track), the fast one is unchanged at 120. One edit changed everything that uses the token. - 2
Spot the quiet failure
Read the console of the same example:
typo button: transition-duration 0s. No error appeared. Click the typo button: it jumps.
Code & diagrams
Click each button twice. At 60 ms the fast button is already 96% of the way (ease-out front-loads the move). The typo button jumps at once.
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
--dur-fast: 120ms
--dur-base: 220ms
--dur-slow: 400ms
--ease-out: cubic-bezier(0.22, 1, 0.36, 1)
--ease-in: cubic-bezier(0.55, 0, 1, 0.45)
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1)
fast button: transition-duration 0.12s
base button: transition-duration 0.22s
slow button: transition-duration 0.4s
typo button: transition-duration 0sindex.html's body, tokens.css, base.css, components.css and main.js merged into one page. Base, components and main are Phase 0's, so Phase 1's loading bar is left out here (your project keeps it). The last script line is only for this example. The page has no motion, and the timeline stays empty.
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
tokens loaded: 220msYour 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.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lumen Volt · Lumen Bikes</title>
<!-- Phase 2: motion tokens (load first, so later files can use them) -->
<link rel="stylesheet" href="/src/styles/tokens.css" />
<!-- end Phase 2 -->
<link rel="stylesheet" href="/src/styles/base.css" />
<link rel="stylesheet" href="/src/styles/components.css" />
</head>
<body>
<header class="site-header">
<a class="logo" id="logo" href="/">lumen</a>
<nav aria-label="Main"><a href="#accessories">Accessories</a> <a href="#gallery">Gallery</a> <a href="#specs">Specs</a></nav>
<button id="cart-button" class="cart-button" aria-label="Cart, 0 items">Cart <span id="cart-count">0</span></button>
</header>
<main>
<section id="hero" class="hero">
<div>
<h1>The Lumen Volt</h1>
<p class="hero-copy">Our first e-bike. 80 km on one charge, built for Ardenvale's hills.</p>
<p class="price">€2,490</p>
<button id="reserve" class="button primary">Reserve yours</button>
<!-- Phase 1: loading the Volt -->
<div class="loading-wrap">
<div class="loading" id="volt-loading" role="progressbar" aria-label="Loading the Volt" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="loading-fill"></div></div>
<p class="loading-label" id="volt-loading-label">Loading the Volt…</p>
</div>
<!-- end Phase 1 -->
</div>
<svg id="volt-hero" class="volt" viewBox="0 0 240 140" role="img" aria-label="The Lumen Volt e-bike">
<g fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
<circle cx="52" cy="98" r="34" />
<circle cx="188" cy="98" r="34" />
<path d="M52 98 L96 98 L132 50 L84 50 Z M96 98 L72 38 M132 50 L188 98 M126 34 L140 34 M132 50 L130 34" />
<path d="M64 38 L82 38" />
</g>
<rect x="98" y="58" width="26" height="14" rx="4" fill="#2a9df4" />
</svg>
</section>
<section id="accessories">
<h2>Accessories</h2>
<div class="grid">
<article class="card"><h3>Lock</h3><p>€39</p><button class="add" data-product="lock">Add</button></article>
<article class="card"><h3>Lights</h3><p>€29</p><button class="add" data-product="lights">Add</button></article>
<article class="card"><h3>Rack</h3><p>€45</p><button class="add" data-product="rack">Add</button></article>
<article class="card"><h3>Bell</h3><p>€9</p><button class="add" data-product="bell">Add</button></article>
<article class="card"><h3>Basket</h3><p>€35</p><button class="add" data-product="basket">Add</button></article>
<article class="card"><h3>Helmet</h3><p>€59</p><button class="add" data-product="helmet">Add</button></article>
</div>
</section>
<section id="gallery">
<h2>Gallery</h2>
<div class="filters" role="group" aria-label="Show colour">
<button class="filter" data-color="all" aria-pressed="true">All</button>
<button class="filter" data-color="orange" aria-pressed="false">Orange</button>
<button class="filter" data-color="blue" aria-pressed="false">Blue</button>
<button class="filter" data-color="ink" aria-pressed="false">Ink</button>
</div>
<div class="photos">
<figure class="photo" data-color="orange"><div class="swatch"></div><figcaption>Volt in orange, side</figcaption></figure>
<figure class="photo" data-color="blue"><div class="swatch"></div><figcaption>Volt in blue, side</figcaption></figure>
<figure class="photo" data-color="ink"><div class="swatch"></div><figcaption>Volt in ink, side</figcaption></figure>
<figure class="photo" data-color="orange"><div class="swatch"></div><figcaption>Orange, at the harbour</figcaption></figure>
<figure class="photo" data-color="blue"><div class="swatch"></div><figcaption>Blue, in the old town</figcaption></figure>
<figure class="photo" data-color="ink"><div class="swatch"></div><figcaption>Ink, at night</figcaption></figure>
</div>
</section>
<section id="specs">
<h2>Specs</h2>
<details class="spec"><summary>Battery</summary><p>500 Wh, removable, 80 km range, full in 4.5 hours.</p></details>
<details class="spec"><summary>Motor</summary><p>250 W rear-hub motor, assists up to 25 km/h.</p></details>
<details class="spec"><summary>Frame</summary><p>Aluminium, three sizes, 21 kg with the battery.</p></details>
</section>
</main>
<footer class="site-footer"><p>Lumen Bikes · Ardenvale</p></footer>
<script type="module" src="/src/js/main.js"></script>
</body>
</html>
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
Rename a token in its definition only
In "The six tokens at work", change --ease-out: cubic-bezier(…) to --easeout: cubic-bezier(…) in the :root rule, and press Run.
Myth vs fact
Myth
Tokens are only for big design systems.
Fact
Three durations and three easings are enough to make a small site consistent, and a single edit then retunes all of it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Custom properties are resolved at computed-value time, which is why a typo does not throw: the cascade has nothing to fall back to. Provide fallbacks where it matters, for example
var(--dur-base, 220ms), or have a test read computed styles (Phase 18). - ▸
A plain custom property is only substituted into other values, never animated by itself. Registering it with
@propertygives it a type the browser can interpolate, which later phases use where needed.
Remember this
- 1
The problem this solves: Mei's storyboard and Kofi's code must say the same thing. With the spec in the repo and the timings as tokens, "fast" means 120 ms in the document and in every stylesheet, and changing it changes the site.
- 2
What it adds.
docs/motion-spec.mdholds the storyboard and a table with one row per motion of the course, with its phase, trigger, durations (as token names) and a reduced-motion answer.src/styles/tokens.cssdefines--dur-fast: 120ms,--dur-base: 220ms,--dur-slow: 400ms,--ease-out,--ease-inand--ease-in-out.index.htmllinks it before the other stylesheets, in a block marked Phase 2. - 3
What the launch site can do now that it could not before: nothing new moves, and that is the point. But it can now answer, for every planned animation, what it is for, how long it takes, how it eases and what Ravi will see. The tokens are available to every stylesheet.
- 4
What is still missing. No component uses a token yet: Phase 3 gives buttons and links their first transitions with them, Phase 4 adds the toast, Phase 5 the grid reveal. Phase 7 makes the reduced-motion column real, and Phase 18 finishes tokens with springs and tests.
- 5
A token typo fails quietly. A name that does not exist makes the declaration invalid at computed-value time, with no console error. The example shows two cases: the
typobutton's transition-duration is 0s, and renaming--ease-outin the definition makes the shorthand using it invalid, so the base button loses its transition completely.
Explain it without notes
Why keep durations and easings as tokens?
What happens to transition: transform var(--dur-base) var(--ease-out) if --ease-out is not defined?
Practice
Add a seventh token, --dur-instant: 0ms, and use it for the keyboard path in Topic 2.2's tooltip.
Add a row to docs/motion-spec.md for a motion you want on your own site.
Done when you can
Your project has docs/motion-spec.md and src/styles/tokens.css
index.html links tokens.css in the Phase 2 block
You can read the six tokens in DevTools
You know why a token typo fails quietly