Shots in this scene
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.
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.
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.
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
Retime the Volt with one number
In
launch-timeline.jschange.addLabel("volt", 0.5)to0.3. The Volt and everything attached tovoltandpricemoves 0.2 s earlier, and the duration printed by the example becomes 1.8 s. - 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 checksmotionAllowed()itself, not only thecalmcondition.
Code & diagrams
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.
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
labels: {"headline":0,"volt":0.5,"price":1} | duration: 2 sYour 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.
{
"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.
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.addcan return a cleanup function. The hero's removesaria-busyand 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
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
What it adds.
launch-timeline.jsexportslaunchTimeline(). With motion allowed it splits the h1 into masked words (aria: "auto"keeps the label), then builds one timeline: the words rise (labelheadline), the Volt slides in atvolt(0.5 s), the price counts from €0 to €2,490 atprice(1 s) witharia-busyset until it ends, and Reserve follows. The Volt's wrapper.hero-artmoves, not the drawing: Phase 6's CSS drift on#volt-herowould override GSAP'sx(measured: the Volt only faded).scroll-story-gsap.jspins#batteryfor 300 px from 700 px wide and only scrubs its meter on a phone.main.jscalls both;package.jsongainsgsap3.15.0, pinned. - 3
How it plugs in. Both files use
motionAllowed()andonMotionChange()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
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
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
How does the hero timeline serve a reduced-motion visitor, and what happens when they change the setting while the page is open?
Why is the battery section placed right after the hero?
Practice
Add a second pinned section for the motor, using initScrollStoryGsap, and make sure leaving the page removes both.
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