Shots in this scene
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.
Before you start
You will learn
- Add
src/styles/scroll.csswith the reading bar and theview()reveal, behind@supports - Add the reading bar and
section#storytoindex.html - Add the observer fallback to
main.js, wired tomotionAllowed()andonMotionChange()
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 theentryrange. - reveal-ready
- A class that
main.jsputs onhtmlonce 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
Force the fallback
In
main.jschangeCSS.supports("animation-timeline", "view()")tofalse. In a browser withview(),htmlnow gets the classreveal-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 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.
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
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 falseThe 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.
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
reading bar: ScrollTimeline · features: 4Your 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.
/* 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.
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 notblock and thereveal-readyclass are two safeguards for one risk: content hidden and never shown. Keep both.
Remember this
- 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
What it adds, and how it plugs in.
scroll.cssholds both effects, each inside@supports:.read-progress-barusesscroll(root block)and.feature.revealusesview()withanimation-range: entry 0% entry 60%. No parallax: features only fade in and rise 24 px.index.htmllinks the file beforemotion-safe.css, adds the bar as the first element of the body, aStorynav link andsection#story.main.jsgets 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
One class, two jobs. Phase 5 already puts
.revealon the product grid (.grid.reveal). A bare.revealrule 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
How it stays calm. Reduced motion and the Phase 7 toggle both switch everything off in CSS:
animation: noneon the bar and features, and the bar hidden. Measured: with the device asking for reduced motion, the bar isdisplay: noneand all four features have opacity 1.data-motion="reduced"does the same on any device, anddata-motion="full"brings the effects back (Phase 7). - 5
The fallback. Where
view()is missing,main.jsstarts anIntersectionObserver(Topic 12.5) and addsreveal-readytohtmlonly 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
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
Why does the reading bar have no fallback, but the reveal does?
How does the page guarantee the features are visible with no script, with no support and with reduced motion?
Practice
Add a fifth feature, Three frame sizes, as an HTML fragment, and say what else you must change.
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