Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 11.7Intermediate ~23 min · shot 7 of 7

Build: The Product to Ride-Page Transition

In one line

You add a ride page, opt both pages in to cross-document transitions so the Volt drawing flies from the launch page to the ride page, slide the ride page's tabs with a same-document transition, and keep all of it calm for reduced motion and safe for browsers without support.

0/7 · 0%

Before you start

You will learn

  • Add a ride page and make the Volt drawing travel to it
  • Slide the ride page's tabs in the direction of travel, also on right-to-left pages
  • Keep a plain navigation for old browsers and a quick fade for reduced motion

You write code in

In your lumen-motion project (npm run dev, then npm run build) and in the editor on this page

Files you touch

  • ride.html
  • src/js/components/transitions.js
  • vite.config.js
  • index.html
  • src/js/main.js
  • src/styles/components.css

Think of it like this

A shop window and the room behind it. If you point at a bike in the window and see that bike travel across the glass into the room, you never lose it. If the window goes dark and a bike appears elsewhere, you wonder whether it is the same bike. The transition is the travelling.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

multi-page build
A Vite build with several HTML files, each listed in build.rollupOptions.input.
opt-in
The @view-transition { navigation: auto; } rule a page needs to take part in cross-document transitions.
withTransition
Our one helper: a transition when the browser supports it and motion is allowed, a plain update otherwise.
bfcache
The back/forward cache: a page kept alive when you leave, so a script-set name can come back with it.

Step by step

01Create the ride page and the config

Add ride.html and vite.config.js to the project root. The ride page's drawing has the same viewBox as the launch page's, so it scales without squashing.

02Opt in inline on both pages

Put <style>@view-transition { navigation: auto; }</style> in both heads (inline: 40 of 40 against 29 to 33 of 40 with a linked file, Topic 11.4).

03Add the script and the CSS

Create transitions.js, add the Phase 11 block to components.css, and call initTransitions() from main.js. The ride page loads it from a small inline module.

04Build it

Run npm run build: both pages are in dist/. Then npm run preview, click See the ride and watch the drawing travel.

terminal
$ npm run build
── expected output ──
vite v8.3.5 building client environment for production...
transforming...
✓ 21 modules transformed.
rendering chunks...
computing gzip size...
dist/ride.html 3.66 kB │ gzip: 1.51 kB
dist/index.html 7.43 kB │ gzip: 2.43 kB
dist/assets/transitions-CBsyt6lH.css 12.11 kB │ gzip: 3.10 kB
dist/assets/ride-C9aVFl4t.js 0.05 kB │ gzip: 0.07 kB
dist/assets/transitions-P6RLXnt3.js 2.93 kB │ gzip: 1.33 kB
dist/assets/main-CdRFXTac.js 10.20 kB │ gzip: 4.26 kB
✓ built in 2.39s

Try it yourself

  1. 1

    Flip the page to right-to-left

    In your project's console run document.documentElement.dir = "rtl", then click the ride page's tabs: the panel leaves toward the right going forwards, because --slide is -1.

  2. 2

    Turn on reduced motion

    In the live page switch on the timeline's Reduced motion and press "See the ride": no tracks appear, because withTransition ran the update directly. In your project choose Reduced in the footer toggle and click the link: a 120 ms cross-fade, and the drawing does not travel.

Code & diagrams

The ride-page transition (both views in one document)

A real page change needs two documents, which one preview cannot show, so the button stands in for the link and swaps the two views. The CSS block and the helper code are cut from your project files. Try the timeline's Reduced motion switch.

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

view transitions: supported
motion allowed: true

Your project files

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

ride.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>Ride the Volt · Lumen Bikes</title>
    <!-- Phase 7: apply the saved motion choice before the first paint (same script as index.html) -->
    <script>
      try {
        var m = localStorage.getItem("lumen-motion");
        if (m === "reduced" || m === "full") document.documentElement.dataset.motion = m;
      } catch (e) {} // storage blocked: follow the device
    </script>
    <!-- end Phase 7 -->
    <!-- Phase 11: opt in, inline on purpose (a rule in a linked file can arrive too late) -->
    <style>
      @view-transition { navigation: auto; }
    </style>
    <!-- end Phase 11 -->
    <!-- Phase 2: motion tokens (components.css reads 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" />
    <link rel="stylesheet" href="/src/styles/motion-safe.css" />
  </head>
  <body>
    <header class="site-header">
      <a class="logo" id="logo" href="/">lumen</a>
      <nav aria-label="Main"><a href="/">Back to the launch page</a></nav>
    </header>
    <main>
      <!-- Phase 11: the ride page (same viewBox as the home page's drawing, so it morphs without stretching) -->
      <section class="hero ride-hero">
        <div>
          <h1>Ride the Lumen Volt</h1>
          <p class="hero-copy">Twelve kilometres across Ardenvale, from the harbour to the old town, on one small charge.</p>
          <a class="button primary" href="/">Reserve yours</a>
        </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 class="ride-details" data-tabs>
        <h2>On the ride</h2>
        <div role="tablist" aria-label="Ride details">
          <button role="tab" id="tab-route" class="tab" aria-selected="true" aria-controls="ride-panel" data-template="t-route">Route</button>
          <button role="tab" id="tab-battery" class="tab" aria-selected="false" tabindex="-1" aria-controls="ride-panel" data-template="t-battery">Battery</button>
          <button role="tab" id="tab-motor" class="tab" aria-selected="false" tabindex="-1" aria-controls="ride-panel" data-template="t-motor">Motor</button>
        </div>
        <div role="tabpanel" id="ride-panel" class="tab-panel" aria-labelledby="tab-route" tabindex="0"><p>Harbour Gate to the old town, 12 km, mostly flat, with one hill the Volt climbs at 25 km/h.</p></div>
        <template id="t-route"><p>Harbour Gate to the old town, 12 km, mostly flat, with one hill the Volt climbs at 25 km/h.</p></template>
        <template id="t-battery"><p>500 Wh. This ride uses about 20 percent of it, so you could do it four times before charging.</p></template>
        <template id="t-motor"><p>250 W rear-hub motor. It assists while you pedal and stops helping at 25 km/h.</p></template>
      </section>
      <!-- end Phase 11 -->
    </main>
    <footer class="site-footer"><p>Lumen Bikes · Ardenvale</p></footer>
    <script type="module">
      import { initTransitions } from "/src/js/components/transitions.js";
      initTransitions();
    </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

Forget a page in vite.config.js

Delete the ride: resolve(...) line, run npm run build, then npm run preview and click the link.

terminal
$ Click "See the ride" in the built site
── what you'll see ──
The link goes to /ride.html, which is not in dist/: the preview server returns the launch page or a 404. `npm run dev` worked, which hid the bug.

Myth vs fact

Myth

Adding a new page is just a new HTML file.

Fact

For Vite's production build the page must also be listed in vite.config.js. The dev server hides this mistake.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Focus stays on the selected tab because show() changes only attributes and the panel's content, so the focused button is never replaced. While the transition runs its picture covers the page (Topic 11.1), so a second click waits until it ends: about 250 ms here, because the panel's group keeps the default 250 ms (measured).

Remember this

  1. 1

    Why the site needs this now. Mei's storyboard has the Volt travelling from the launch page to the ride page, and the reload between them flashes white. The launch page also needs somewhere to go: this phase adds ride.html, with the same drawing, big, and tabs for route, battery and motor.

  2. 2

    What the build adds and how it plugs in. ride.html, src/js/components/transitions.js (initTransitions(), withTransition()) and vite.config.js are new. index.html gains a "See the ride" link and the inline opt-in; components.css gains the names, durations and keyframes; main.js calls initTransitions(). Between pages CSS does the work. transitions.js adds what CSS cannot know: on pageswap it drops the drawing's name if the drawing is scrolled out of view (measured: only new(volt) remained, so the page cross-fades) and restores it afterwards for the back/forward cache. The tabs call withTransition(update, ["forwards" | "backwards"]), which uses Phase 7's motionAllowed() and falls back to a plain update. The three changed files below are complete: they are your project after Phases 9 and 10 (the cart drawer and the gallery), with this phase's marked Phase 11 blocks added. ride.html also loads tokens.css, which the ride page's styles read.

  3. 3

    Reduced motion, measured. On reduced motion (device setting or the Phase 7 footer toggle) the drawing's group lasts 0 ms and the pictures cross-fade for 120 ms; the tabs make no transition. The tab slide flips with dir: at half of the old panel's 120 ms, translateX(-3.2px) forwards left-to-right and +3.2px right-to-left (the project's --ease-in token starts slowly, so little has moved by then).

  4. 4

    Build and run. npm run dev serves both pages; npm run build bundles both because vite.config.js lists them. Without the config only index.html would be built and the link would break in production.

  5. 5

    What the launch site can do now that it could not before: the Volt flies between the two pages (cross-document support 88.81%, caniuse 2026-10-19), the tabs slide the right way, and visitors on reduced motion or an older browser get a quick fade or a plain navigation. What is still missing: the scroll story (Phase 12), the animated logo and route line (Phase 13), the GSAP launch sequence (Phase 14).

Explain it without notes

01

Which parts of this build are cross-document and which same-document, and what does each need? What does a browser with neither support get?

02

Why is the opt-in rule inline in the head rather than in components.css?

Practice

01

Add a fourth tab, "Warranty", with its own template, and check that the slide direction still works.

02

Add a second product card that links to the ride page, and make pageswap name only the clicked card's drawing.

Done when you can

  • You can add a second page to a Vite project and list it in the config

  • You can make a drawing travel between two pages with a shared name

  • You can slide tabs in the direction of travel and flip them for right-to-left