Web Development1 June 2026 · 6 min read

Behind the build: Veloce, a car dealer template that actually moves

Most car dealer websites look like 2008 called and wanted its inventory grid back. Veloce is the first of five templates I shipped this week — a GSAP-driven dark car dealer build with cinematic video plates, sticky scroll heroes, and three bugs I caught in QA.

Behind the build: Veloce, a car dealer template that actually moves

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.

DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Senior full-stack developer shipping SaaS MVPs, web apps, and mobile apps 2× faster than agencies using AI-augmented workflows. Live portfolio: BookBed, Callidus, Pizzeria Bestek.