Most car dealer websites look like 2008 called and wanted its inventory grid back. Filters that don't filter. Stock photos of cars the dealer doesn't sell. A "Book service" page that's a form behind two redirects. I wanted to build the opposite — a template that treats the visitor like an adult and gets out of the way.
Veloce is the first of five templates I shipped this week, all under the same constraint: no build step, no React-Router, no SPA bloat. Drop the folder on Netlify and it works.
Stack picks (and what I cut)
The whole thing runs on:
- Static HTML per page (
Veloce.html,Inventory.html,Service.html,Financing.html,About.html,Vehicle.html) - React 18 UMD from a CDN
- In-browser Babel for JSX
- GSAP for hero scroll-driven motion
- Tailwind via Play CDN
That setup gets a Tailwind warning in the console — "not for production". I read the warning and shipped anyway. The whole site is under 2 MB compressed and serves from CDN edge. Reverting to a Vite build would have added a config file, a node_modules, and a deploy step. Nothing the visitor would feel.
What I cut: a React-Router replacement (HTML pages link to each other the normal way), an SSR layer, and an image CMS. Five HTML files do not need a CMS.
The hero
Veloce's hero is a vertical-scroll slider — but instead of fading between three cinematic video plates, GSAP drives a clip-path ellipse that opens from 5% to 150% of the viewport. The effect: each new vehicle plate "blooms" out from the center.
const slideDist = (SCROLL_PER_SLIDE_VH / 100) * vh;
const scrolled = -wrapperRef.current.getBoundingClientRect().top;
const p = (scrolled - idx * slideDist) / slideDist;
const e = easeInOut(clamp01(p));
ref.style.clipPath = `ellipse(${5 + e * 150}% ${8 + e * 150}% at 50% 50%)`;
A requestAnimationFrame debounce keeps it at 60fps even on a mid-tier Android. The whole hero is one position: sticky wrapper that's 100vh + 300vh tall — three slide stops, then the page continues.
Video pipeline
Every hero plate is a Gemini Veo generation, transcoded down to 1280-wide H.264 at CRF 26 with +faststart for instant playback. The Veo sparkle watermark in the bottom-right of the 1920×1080 source gets blurred out with ffmpeg's delogo filter at x=1700 y=880 w=120 h=110. The whole hero — three video plates plus 22 product photos — lands at about 7 MB.
I trimmed the third hero plate from 10s to 4s after watching the rough cut. The studio shot did not have enough new information to earn the extra seconds.
Reveal animation: the un-blink
Every section enters the viewport with an IntersectionObserver one-shot — element starts at opacity: 0 and translateY(30px), gains an .in class on intersect, transitions to visible. Standard pattern.
On desktop it reads as cinematic. On mobile it reads as: every section blinks like an eye as you scroll. I cut the translate from 30px to 8px and the transition from 0.85s to 0.45s. The motion is still there. The blink is not.
Three bugs I caught in QA
1. The bfcache zombie. A page-exit overlay (a full-bleed black <div> that fades in on link click for the route transition) persisted in the bfcache. Hit browser-back from Vehicle.html to the homepage and the overlay was still there — black screen on every back navigation. Fix: listen for pageshow with event.persisted === true and popstate, force the overlay opacity back to 0.
2. The 1-image gallery. Some vehicle listings have one studio photo. The thumb-row component was rendering a single thumbnail that the user could click — to nothing. Hide the row when images.length === 1.
3. The hero stack order on mobile. The hero block puts the title in a right column and the kicker/subtitle/CTAs in a left column. On desktop, they sit side by side. On mobile, the flex direction collapses to a column — and the JSX order put the left block first. Result: kicker → subtitle → CTAs → title. Fix: order-1 on the title with lg:order-2, mirror inverse on the left block. Title leads on mobile, sits right on desktop.
Live
veloce-template-01.netlify.app — open it on mobile and scroll the hero.
