Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 14.7Advanced ~45 min · shot 7 of 7

Build: The Launch-Day Hero Timeline

In one line

Give the hero a launch-day sequence in one labelled GSAP timeline (headline, Volt, price count-up) and add a pinned battery section, both switched off for reduced motion and both removable without leaving anything behind.

0/7 · 0%

Before you start

You will learn

  • Install GSAP 3.15.0 and add the hero timeline and the pinned battery story to your project
  • Keep the headline's name for screen readers, and show the finished page to reduced-motion users
  • Remove everything cleanly when the setting changes

You write code in

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

Files you touch

  • package.json
  • src/js/gsap/launch-timeline.js
  • src/js/gsap/scroll-story-gsap.js
  • index.html
  • src/styles/components.css
  • src/js/main.js

Think of it like this

The trailer for a film. The film already exists; the trailer chooses what to show first, how the cuts overlap and when the title card lands. The launch page already works; this phase adds the trailer, and a viewer who skips it still gets the whole film.

Words you'll meet

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

label
A named time in the timeline (headline, volt, price) that tweens attach to, so one number retimes a whole group.
aria-busy
An attribute that tells assistive technology an element is still changing, so it waits for the final value.
pin spacer
The wrapper that keeps room for the pinned section's scroll distance.
bundle
The JavaScript a browser downloads.

Step by step

01Install GSAP, pinned

In your project folder add GSAP at an exact version. --save-exact writes 3.15.0 without a caret, so the whole team gets the same library.

terminal
$ npm install gsap@3.15.0 --save-exact
── expected output ──
added 1 package, and audited 2 packages in 2s
 
found 0 vulnerabilities
From a scratch project on 2026-10-23; your package count will include Vite and its tools.

02Add the two modules and the markup

Create src/js/gsap/launch-timeline.js and scroll-story-gsap.js, add the #battery section after the hero in index.html, its small CSS block at the end of components.css, and the two calls at the end of main.js. Every addition sits in a Phase 14 marked block.

03Build for production and read the size

Run the build. The page's JavaScript grows by about 122 kB: that is GSAP's core, SplitText and ScrollTrigger. Import only the plugins you use (gsap/SplitText, not gsap/all) so unused ones stay out.

terminal
$ npm run build
── expected output ──
vite v8.3.5 building client environment for production...
✓ 20 modules transformed.
dist/index.html 6.41 kB │ gzip: 2.07 kB
dist/assets/index-CS47HNwr.css 5.99 kB │ gzip: 1.86 kB
dist/assets/index-CAH5Uh5k.js 127.82 kB │ gzip: 50.57 kB
✓ built in 353ms
A real run with Vite 8.3.5 on the Phase 10 project plus the Phase 14 files: before Phase 14 the JavaScript file was 5.99 kB (2.63 kB gzipped). Your numbers also include what Phases 11 and 12 added.

04Look at the page, then switch motion off

Run npm run dev and reload: the headline words rise, the Volt slides in and the price counts up. In the footer choose Reduced: the headline is whole at once, the price is €2,490 and nothing animates. Measured in headless Chromium: with reduced motion the h1 had no pieces and no aria-label; with motion it had the label The Lumen Volt and 6 divs (3 words, 3 masks).

Try it yourself

  1. 1

    Retime the Volt with one number

    In launch-timeline.js change .addLabel("volt", 0.5) to 0.3. The Volt and everything attached to volt and price moves 0.2 s earlier, and the duration printed by the example becomes 1.8 s.

  2. 2

    Switch the site to Full while the device says reduce

    With the device set to reduced motion, choose Full in the footer. motionAllowed() now answers true and the timeline is built: the site's own choice wins, because the function checks motionAllowed() itself, not only the calm condition.

Code & diagrams

The hero and the battery, as one page

The finished hero and battery of the launch page with the project files inlined into one script (local imports cannot run in the preview). Press Replay to see the sequence, switch Reduced motion on to see the page finished at once, and scroll the viewfinder to the battery.

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

labels: {"headline":0,"volt":0.5,"price":1} | duration: 2 s

Your project files

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

package.jsonjson

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

{
  "name": "lumen-motion",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "gsap": "3.15.0"
  },
  "devDependencies": {
    "vite": "8.3.5"
  }
}

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

Leave out the cleanup function

In launch-timeline.js delete the function the matchMedia callback returns (the return () => { … } block). Load the page, wait 1.5 s while the price is counting, then choose Reduced in the footer.

terminal
$ Read the price's text and aria-busy after choosing Reduced
── what you'll see ──
price "€2,490", aria-busy "true" (with the cleanup: aria-busy is gone)

Myth vs fact

Myth

Reduced-motion users should get a shorter version of the animation.

Fact

They should get the finished page. If someone asked for less motion, a faster version is still motion; the content must simply be there.

Pro corner

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

  • ▸

    mm.add can return a cleanup function. The hero's removes aria-busy and writes the final price: the count-up may be reverted mid-way, and the page must not be left reading €1,042.

Remember this

  1. 1

    The problem this solves: Tomás wants a launch-day moment and Mei wants to retime it without asking for code. Ravi needs a page that is complete without motion and a headline that is still one sentence to his screen reader. One labelled timeline answers all three: Mei changes a label's time, and gsap.matchMedia() decides whether the sequence exists at all.

  2. 2

    What it adds. launch-timeline.js exports launchTimeline(). With motion allowed it splits the h1 into masked words (aria: "auto" keeps the label), then builds one timeline: the words rise (label headline), the Volt slides in at volt (0.5 s), the price counts from €0 to €2,490 at price (1 s) with aria-busy set until it ends, and Reserve follows. The Volt's wrapper .hero-art moves, not the drawing: Phase 6's CSS drift on #volt-hero would override GSAP's x (measured: the Volt only faded). scroll-story-gsap.js pins #battery for 300 px from 700 px wide and only scrubs its meter on a phone. main.js calls both; package.json gains gsap 3.15.0, pinned.

  3. 3

    How it plugs in. Both files use motionAllowed() and onMotionChange() from Phase 7: choose Reduced in the footer and the timeline is reverted, the headline whole and the price €2,490. initScrollStoryGsap() returns a cleanup function. The battery section sits right after the hero, because a pin near the end of a page can run out of page: it needs at least a screen of content after the section for the meter to finish (Topic 14.3).

  4. 4

    What the launch site can do now: it opens with a sequence the designer retimes with one number per step, counts the price up without a screen reader hearing every number, pins a section on desktop and scrubs it plainly on a phone, and undoes all of this live when the motion setting changes. Measured with Vite 8.3.5: GSAP with SplitText and ScrollTrigger adds about 122 kB of JavaScript (about 48 kB gzipped).

  5. 5

    What is still missing: the drawer and the gallery keep our own spring and FLIP helpers, not GSAP. Phase 15 builds the React configurator with Motion, and Phase 16 the headlight mascot. Phase 12's CSS story and this pin are alternatives: ship one per section. Motion tokens and tests come in Phase 18.

Explain it without notes

01

How does the hero timeline serve a reduced-motion visitor, and what happens when they change the setting while the page is open?

02

Why is the battery section placed right after the hero?

Practice

01

Add a second pinned section for the motor, using initScrollStoryGsap, and make sure leaving the page removes both.

02

Add a label ready at the very end of the hero timeline, and use it to jump straight to the finished hero.

Done when you can

  • You can say what each label in the hero timeline attaches

  • You can show the page is complete with Reduced selected

  • You know where a pinned section can and cannot start