A cinematic, single-page scrollytelling experience built around the Kawasaki Ninja H2R. Seven sections take the visitor from a black screen and a cold ignition to 400 km/h, then drop them into silence for a production-allocation request.
No build step. No framework. Plain HTML, CSS and JavaScript with GSAP driving every scroll-linked moment.
- Highlights
- The seven sections
- Tech stack
- Project structure
- Running it locally
- How it works
- Performance notes
- Accessibility
- Browser support
- Credits and licensing
- Letter-level text shredding. Every glyph is split into five horizontal strips of
irregular height, cut with percentage
clip-path. On impact a light bar sweeps the phrase and each strip flies to its own side — the text shreds like paper, it does not simply fade. - Cyberpunk camera shake. Randomised, decaying keyframes on a single wrapper element, escalating 26 → 38 → 44 across the three image transitions, plus a continuous 80-frame rumble through the climax.
- Scroll-driven odometer. A fixed-width digital speedometer climbing 0 → 200 → 300 → 400, locking at 400 with an aggressive red alarm.
- Blueprint materialisation. A pinned five-stage sketch with leader lines drawn from anchor dot to callout box, accumulating part by part.
- Hard cut between sections. The rumble's residual transform is explicitly cleared so the conversion section inherits a perfectly still, razor-sharp frame.
- Zero dependencies to install. GSAP and the fonts load from a CDN.
| # | Section | What happens |
|---|---|---|
| 1 | Hero | Headlights blink twice, then burn on. Subtle zoom, scroll hint fades away. |
| 2 | Components | Bento grid of nine Kawasaki parts (supercharger, motor, painting, chassis, brakes, air, shock, shifter, acceleration). Each card is a full-bleed photo; the description slides up from the bottom on hover or keyboard focus. |
| 3 | Sweep | Five side-on frames lit laterally, captions stepping in time with the light. |
| 4 | Ignition | Muted looping video that only decodes while the section is on screen. |
| 5 | Sketch | Pinned blueprint resolving from wireframe to full motorcycle, four callouts drawing in with measured leader lines. |
| 6 | 400 km/h | Full scrollytelling sequence: text shredding, escalating shake, speed trails intensifying, odometer to 400. |
| 7 | Conversion | The silence after the storm. Close-up of the front wheel with a faked heat glow on the Brembo disc, glassmorphism panel, neon form. |
| Layer | Choice |
|---|---|
| Markup | Semantic HTML5, lang="en" |
| Styling | Plain CSS — custom properties, Grid, Flexbox, clip-path, backdrop-filter |
| Animation | GSAP 3.12.5 + ScrollTrigger (CDN) |
| Fonts | Syncopate (display), Inter (body), IBM Plex Mono (technical) via Google Fonts |
| Build tooling | None |
.
├── index.html
├── css/
│ ├── base.css reset, base typography, .sr-only utility
│ └── sections/
│ ├── hero.css
│ ├── components.css
│ ├── sweep.css
│ ├── ignition.css
│ ├── sketch.css
│ ├── speed.css
│ └── conversion.css
├── js/
│ ├── main.js gsap.registerPlugin — loaded first
│ ├── hero.js
│ ├── components.js
│ ├── sweep.js
│ ├── ignition.js
│ ├── sketch.js
│ ├── speed.js
│ └── conversion.js
└── assets/
├── images/
│ ├── hero/ h2r-lights-off|on.jpg, logo.png, logo-cutout.png
│ ├── sweep/ side-0..4.jpg
│ ├── components/ nine component photographs
│ ├── speed/ speed-1..3.jpeg
│ └── sketch/ sketch-1..5.jpeg
└── video/
└── turning-on.mp4
CSS and JavaScript are split per section and in page order, so opening
css/sections/speed.css next to js/speed.js gives you everything that section needs.
Two naming conventions keep the long single page safe:
- Sections that were added late are prefixed to avoid colliding with existing rules —
.sr-*for the 400 km/h sequence,.cv-*for the conversion section. - The JavaScript blocks are wrapped in IIFEs, so section-local constants never leak into the global scope or into each other.
There is nothing to install or build. Because the page loads a video and a handful of
JPEGs, open it through a local server rather than file://:
# any static server works
python3 -m http.server 8000
npx serve .Then visit http://localhost:8000.
If you open index.html directly from the filesystem the layout still renders, but some
browsers restrict video playback on the file:// protocol.
The speed section is a single gsap.timeline() with scrub: 1, pinned for eight viewport
heights. Because scrub maps scroll position onto timeline position, every effect has
to be a tween inside that timeline. Firing a shake from a callback with
timeline.add(() => gsap.to(...)) creates a real-time tween that desynchronises the moment
the user scrolls back up. The helper functions here return vars instead of tweens
(shakeVars(), flashVars(), rumbleVars()) and the timeline does the executing.
const S = { s0: 0.0, s1: 1.2, s2: 3.2, s3: 5.2, end: 8.0 };The odometer's alarm threshold is derived from these rather than hard-coded, so moving a stage boundary updates everything downstream.
gsap extends a tween's duration by the whole stagger window. Hiding a line at
at + duration — while ignoring the stagger — makes text vanish mid-flight. Both
showLine() and tearLine() compute the real duration from the element count and return
it, which also keeps the entrance/teardown pair consistent.
Each character becomes five absolutely positioned strips clipped with percentage
clip-path boundaries, jittered per letter so the cuts are never ruler-straight. A
hidden sizer span sits in flow to give the letter its box. Words are wrapped in .sr-word
(inline-block + nowrap) — without it the browser breaks between adjacent inline-blocks
and shreds words down the middle.
Entries animate the ~40 letters; exits animate the ~200 strips. Animating every strip on the way in would be expensive and would not read any differently.
- Images total ~47 MB at full resolution. Convert to WebP/AVIF and add responsive
srcsetbefore shipping this to production. will-changeis deliberately not set on the text strips — with ~200 per phrase that would allocate hundreds of GPU layers. GSAP promotes them for the duration of the tween instead.- The ignition video is paused whenever its section leaves the viewport, so it stops decoding off-screen.
- Only one line of text animates at a time.
- Every animated phrase keeps its full text in an
aria-label; the generated letters and strips arearia-hidden. Screen readers never hear a shredded sentence. - Interactive cards are
tabindex="0"with a:focus-visiblering, so the hover reveal is reachable by keyboard. Without the tabindex,:focuscould never fire. - The odometer is decorative and marked
aria-hidden; its information is in the copy. prefers-reduced-motion: reducecollapses all shakes, strip animations and the odometer alarm to near-zero duration.- Form labels are bound to their inputs, and the status line uses
role="status"witharia-live="polite".
Chrome, Edge, Firefox and Safari (desktop and mobile), current versions. The page relies on
CSS clip-path, custom properties, backdrop-filter, Grid and GSAP ScrollTrigger.
Product data for the component section is adapted from Kawasaki's official Ninja H2R product page.
⚠️ Image licensing. Every photograph inassets/images/— the studio shots, the speed frames, the blueprint and the nine component close-ups — is Kawasaki product photography and remains the property of Kawasaki Heavy Industries. They are included here for local development and portfolio demonstration. Obtain the appropriate licence before publishing this project, or swap them for imagery you own.
The conversion form has no backend wired up. Submission is intercepted client-side for
validation and a neutral confirmation; nothing is transmitted. Point the <form> at a real
endpoint to collect leads.
Trademarks: Kawasaki, Ninja and H2R are trademarks of Kawasaki Heavy Industries, Ltd. This project is not affiliated with or endorsed by Kawasaki.
Built as a front-end portfolio piece.