Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 12.7Intermediate ~78 min · shot 7 of 7

Build: The Volt Scroll Story

In one line

Give the launch page a reading bar and a story section whose four features fade in as they arrive, both driven by the scroll bar in CSS, with an IntersectionObserver fallback for browsers without view(), and all of it off for reduced motion and the site toggle.

0/7 · 0%

Before you start

You will learn

  • Add src/styles/scroll.css with the reading bar and the view() reveal, behind @supports
  • Add the reading bar and section#story to index.html
  • Add the observer fallback to main.js, wired to motionAllowed() and onMotionChange()

You write code in

In your lumen-motion project (npm run dev) and in the editor on this page: add scroll.css, change index.html and src/js/main.js

Files you touch

  • src/styles/scroll.css
  • index.html
  • src/js/main.js

Think of it like this

The chapter marks in a book. They add nothing to the story but show how far you are, and a book without them is still a book. The reading bar and the reveal are chapter marks: the page is complete without them.

Words you'll meet

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

reading bar
A thin bar at the top of the page that fills as the reader scrolls: a scroll-driven animation on scroll(root).
reveal
A feature that fades in and rises as it enters view: a view() animation over the entry range.
reveal-ready
A class that main.js puts on html once the fallback observer exists. The CSS hides cards only under it.
@supports not
A block that applies only in browsers that lack a feature: here the fallback's hidden state.

Step by step

01Add scroll.css and link it

Create src/styles/scroll.css and link it in index.html before motion-safe.css. Its reduced-motion rules use the same data-motion attribute as Phase 7.

02Add the bar and the story

Put the .read-progress div as the first element of the body (it is aria-hidden: it is decoration), add the Story nav link, and add section#story with four article.feature.reveal before </main>.

03Add the fallback to main.js

Paste the Phase 12 block at the end of main.js. It does nothing in a browser with view().

04Test the three paths

In "The reveal's fallback" the old-browser path reveals cards as you scroll (1 of 4 at load, 4 of 4 after four steps) and shows all when motion is switched off. In "The launch page with its story" use the Reduced motion switch: the bar goes and the features show at once.

Try it yourself

  1. 1

    Force the fallback

    In main.js change CSS.supports("animation-timeline", "view()") to false. In a browser with view(), html now gets the class reveal-ready (measured), because the observer starts. The reveal itself is unchanged: its hidden state sits inside @supports not (animation-timeline: view()), which does not apply here. That is why the fallback has to be tested with the support flag passed in, as the first example does.

Code & diagrams

The reveal's fallback

The logic of the `main.js` block with the support test passed in, so you can test the old-browser path here. It uses a box as the observer's root, where the page uses the viewport.

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

old browser, motion allowed: reveal-ready true, shown at load 1 of 4
scrolled to the end in steps: shown 4 of 4
motion switched off: reveal-ready false, every card visible: true
browser with view(): the observer never starts, reveal-ready false
The launch page with its story

The page as it stands after this phase: `index.html`'s body, every stylesheet, and the scripts inlined (local imports do not run in the preview). Scroll to the end and read the bar and the features.

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

reading bar: ScrollTimeline · features: 4

Your project files

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

src/styles/scroll.csscss

The whole file after this topic. If yours differs, you can copy this one and carry on.

/* scroll.css (Phase 12): a reading bar, and features that fade in as they enter the screen.
   Both are enhancements: without scroll-driven animation the page is complete. main.js adds an observer fallback. */

/* ── Phase 12: the reading bar and the story ── */
.read-progress { position: fixed; inset: 0 0 auto 0; height: 4px; z-index: 40; pointer-events: none; }
.read-progress-bar { height: 100%; background: var(--orange); transform: scaleX(0); transform-origin: left; }

.story { padding-bottom: 24px; }
.features { display: grid; gap: 28px; }
.feature { padding: 28px 20px; border-radius: 14px; background: #fff; border: 1px solid #0001; }
.feature h3 { margin: 0 0 6px; }
.feature p { margin: 0; }

@keyframes read-grow { to { transform: scaleX(1); } }
@keyframes reveal-in { from { opacity: 0; transform: translateY(24px); } }

/* The native way: the scroll bar is the clock. Only where the browser understands it. */
@supports (animation-timeline: scroll()) {
  .read-progress-bar { animation: read-grow linear both; animation-timeline: scroll(root block); }
}
@supports (animation-timeline: view()) {
  /* No parallax: the feature only fades in and rises 24px, finishing when 60% of it has entered. */
  .feature.reveal { animation: reveal-in linear both; animation-timeline: view(); animation-range: entry 0% entry 60%; }
}

/* The fallback: main.js adds .reveal-ready and .is-visible where view() is missing. */
@supports not (animation-timeline: view()) {
  .reveal-ready .feature.reveal { opacity: 0; transform: translateY(24px); transition: opacity var(--dur-slow, 400ms) var(--ease-out, ease-out), transform var(--dur-slow, 400ms) var(--ease-out, ease-out); }
  .reveal-ready .feature.reveal.is-visible { opacity: 1; transform: none; }
}

/* Reduced motion, and the site toggle from Phase 7: everything above goes. The content and the page stay. */
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) :is(.feature.reveal, .read-progress-bar) { animation: none; }
  :root:not([data-motion="full"]) .read-progress { display: none; }
}
:root[data-motion="reduced"] :is(.feature.reveal, .read-progress-bar) { animation: none; }
:root[data-motion="reduced"] .read-progress { display: none; }
/* ── end Phase 12 ── */

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 features in plain CSS

In scroll.css add .reveal { opacity: 0; } outside any @supports block.

terminal
$ Scroll to the end of the page, with and without the Reduced motion switch
── what you'll see ──
The four features have opacity 0 in both runs (measured, after scrolling to the end): they never appear.

Myth vs fact

Myth

Reduced motion means no reveal, so the content may be hidden.

Fact

It means no movement. The content must still be on the page, readable and visible. Here the features are simply visible from the start.

Pro corner

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

  • ▸

    The @supports not block and the reveal-ready class are two safeguards for one risk: content hidden and never shown. Keep both.

Remember this

  1. 1

    The problem this solves (Tomás's request, closed): the specs were a list of folded boxes, and the launch page had no story. Now a thin orange bar fills as you read, and four features arrive one at a time, in CSS, with no scroll listener.

  2. 2

    What it adds, and how it plugs in. scroll.css holds both effects, each inside @supports: .read-progress-bar uses scroll(root block) and .feature.reveal uses view() with animation-range: entry 0% entry 60%. No parallax: features only fade in and rise 24 px. index.html links the file before motion-safe.css, adds the bar as the first element of the body, a Story nav link and section#story. main.js gets the fallback block at the end. The files continue the project of Phases 9 and 10 (drawer, gallery), and the launch-page example shows them all running.

  3. 3

    One class, two jobs. Phase 5 already puts .reveal on the product grid (.grid.reveal). A bare .reveal rule here would also animate and observe that grid (measured: 5 elements match .reveal, 4 match .feature.reveal), so every rule and the observer say .feature.reveal. Renaming Phase 5's class (say to .stagger) later is safe.

  4. 4

    How it stays calm. Reduced motion and the Phase 7 toggle both switch everything off in CSS: animation: none on the bar and features, and the bar hidden. Measured: with the device asking for reduced motion, the bar is display: none and all four features have opacity 1. data-motion="reduced" does the same on any device, and data-motion="full" brings the effects back (Phase 7).

  5. 5

    The fallback. Where view() is missing, main.js starts an IntersectionObserver (Topic 12.5) and adds reveal-ready to html only once it exists. It reveals once, counts a card above the screen as seen, and stops when motion is switched off. The bar has no fallback: older browsers show no bar and lose nothing.

  6. 6

    What the launch site can do now: it tells the Volt's story as you scroll and shows how far you have read, in CSS where supported, with an observer where not, and plainly with reduced motion. What is still missing: the route that draws itself (Phase 13), a GSAP pinned battery section (Topic 14.7), the launch timeline (Phase 14) and the mascot (Phase 16).

Explain it without notes

01

Why does the reading bar have no fallback, but the reveal does?

02

How does the page guarantee the features are visible with no script, with no support and with reduced motion?

Practice

01

Add a fifth feature, Three frame sizes, as an HTML fragment, and say what else you must change.

02

Make the reading bar follow only the story section instead of the whole page.

Done when you can

  • Your project has scroll.css, the bar, section#story and the fallback block in main.js

  • The bar fills as you scroll and the features fade in

  • With reduced motion, or the toggle on Reduced, the bar is gone and every feature is visible

  • Without view() support the observer reveals the features, and stops when motion is switched off