Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 2.6Beginner ~24 min · shot 6 of 6

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.

0/6 · 0%

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

    Change one token

    In "The six tokens at work", change --dur-base: 220ms to 900ms and 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. 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

The six tokens at work

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.

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

--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 0s
The launch page after Phase 2

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

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

tokens loaded: 220ms

Your project files

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

index.htmlhtml

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.

terminal
$ Read the console
── what you'll see ──
--ease-out:
base button: transition-duration 0s
Verified in a run: the `--ease-out` line is empty, and the base button has no transition. The fast and slow buttons still animate, but with the default `ease` curve.

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 @property gives it a type the browser can interpolate, which later phases use where needed.

Remember this

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

    What it adds. docs/motion-spec.md holds 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.css defines --dur-fast: 120ms, --dur-base: 220ms, --dur-slow: 400ms, --ease-out, --ease-in and --ease-in-out. index.html links it before the other stylesheets, in a block marked Phase 2.

  3. 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. 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. 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 typo button's transition-duration is 0s, and renaming --ease-out in the definition makes the shorthand using it invalid, so the base button loses its transition completely.

Explain it without notes

01

Why keep durations and easings as tokens?

02

What happens to transition: transform var(--dur-base) var(--ease-out) if --ease-out is not defined?

Practice

01

Add a seventh token, --dur-instant: 0ms, and use it for the keyboard path in Topic 2.2's tooltip.

02

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