A cinematic, scroll-animated café site built as a sales demo you can show local cafés and restaurants, then re-skin per client.
Plain HTML + CSS + JS. GSAP, ScrollTrigger, SplitText and Lenis from CDN. No build step, no framework, no npm install.
python -m http.server 8777Then open http://127.0.0.1:8777. Any static server works — it is just files.
Drag the folder onto Netlify Drop, or push it to a GitHub repo and turn on Pages. Nothing to compile.
bloom-cafe/
├─ index.html all markup, one page
├─ css/style.css design system + every section
├─ js/main.js the whole motion spine
├─ assets/img/ 8 photos (Pexels licence — commercial use, no attribution)
├─ BRAND.md the brand decisions this build was made from
└─ README.md
The opener is a mask zoom-through. The page lands as a solid paper field with
BLOOM cut out of it, so the café plays inside the letterforms. Scrolling scales the
SVG <text> 46× and flies the camera through the first O into the fullscreen room.
One thing worth knowing if you re-skin this: the zoom origin must sit inside a
letter's stroke, not inside the counter. Whichever region contains the origin is
the region that grows to fill the screen — aim at the counter and you fly into a
blank paper field instead of into the photo. That is set in layoutStencil() in
js/main.js.
- Fill in
BRAND.mdfirst. Name, what they refuse to be, the one feeling. Do not open a code file before this is decided — the copy carries the site more than the animation does. - Palette: four values at the top of
css/style.css(--paper,--ink,--accent,--muted). Keep it to one accent. A rainbow reads cheap. - Type: the two
<link>tags inindex.htmlplus--display/--body/--mono. Keep one display face with real presence. - The wordmark: change
BL<tspan id="targetO">O</tspan>OMinindex.html. Wrap whichever letter you want to fly through in thetargetOtspan — it needs a closed counter (A, B, D, O, P, R). If the client's name has none, switch to a different opener rather than forcing this one. - Photos: replace the 8 files in
assets/img/, keeping the filenames. Client photography beats stock every time. - Copy, menu rows, hours, prices: all plain HTML.
Pick a different signature move each time or your builds start looking like a template — which is the one thing that kills the price.
The eight photos are from Pexels: free for commercial use, no attribution required, so they are safe to ship in paid client work. They are downloaded locally rather than hotlinked, so nothing breaks if Pexels changes a URL.
Two originally-shortlisted photos were dropped because other businesses' signage and logos were visible in them. Watch for that when you swap images — incidental third-party branding in a client's site is a problem you do not want to inherit.
- Fonts gate — content stays hidden until fonts are ready, so split text never measures against a fallback face.
- Reduced motion —
prefers-reduced-motionshows the full page settled, zero motion. - Static fallback — if the CDN is unreachable,
is-staticreveals everything readable instead of leaving a blank page. Tested by pointing the CDN at a dead host. - No scroll trap — the pinned section is desktop-only; mobile never pins.
Chrome at 1440×900 and 390×844, scrolled full-page down and back up; all scrubs reverse cleanly, no horizontal overflow at either width.