Ninth Wave looks like a screen-printed surf poster that learned to scroll — but it is one HTML file, one stylesheet and one script. Two ideas carry it: a product shelf that drifts sideways as the page scrolls (and lets you grab it), and a rotating rubber-stamp badge drawn entirely in SVG.
Surf brands usually sell the wave. Ninth Wave sells the waiting — the eight
disappointments before the good one — so the site had to feel like the noticeboard of a
cold-water surf shop: sun-bleached paper, a rubber stamp, a surf report written by someone
salty, and a shelf of heavy garments you browse sideways. The palette is beach-at-noon
(pale sand, washed denim, faded coral), deliberately opposite to the deep-blue ocean sites
everyone else ships. Every motion uses one loose, oceanic ease —
cubic-bezier(.24,1.12,.36,1) — a slow settle with a slight overshoot, like water
finding its level.
IntersectionObserver + requestAnimationFrame.feTurbulence data-URI; the badge is hand-authored SVG.The product shelf is a horizontal track inside a pinned section. ScrollTrigger pins the shelf
when its center hits the viewport center, then scrubs the track's x across exactly
its own overflow — so page scroll becomes sideways drift, with a 1.1s scrub lag that
makes the cards feel like they're floating on chop rather than bolted to the scrollbar:
const overflow = () => track.scrollWidth - innerWidth;
gsap.to(track, {
x: () => -overflow(),
ease: 'none',
scrollTrigger: {
trigger: shelf,
start: 'center center',
end: () => '+=' + overflow(), // 1px of scroll = 1px of drift
pin: true,
scrub: 1.1, // the loose, oceanic lag
invalidateOnRefresh: true
}
});
The part most implementations get wrong is drag. Instead of a second animation system fighting the scrub, dragging the shelf simply maps pointer delta onto page scroll — the scrub stays the single source of truth, so mouse wheel, keyboard, scrollbar and grab all agree:
track.addEventListener('pointermove', (e) => {
if (!dragging) return;
window.scrollBy(0, -(e.clientX - lastX)); // drag left → scroll down → drift left
lastX = e.clientX;
});
On screens under 861px — and for anyone with prefers-reduced-motion — the same
markup falls back to an honest overflow-x: auto row with scroll-snap. GSAP's
matchMedia() adds and removes the whole behavior, cleanup function included.
The badge is one SVG: a circle of Bebas Neue set on a circular textPath, a dashed
inner ring, and a hand-drawn two-stroke wave in the middle. Only the text ring rotates — the
wave stays level, like a real stamp pressed slightly off-true:
<defs>
<path id="badge-arc" d="M100,100 m-72,0
a72,72 0 1,1 144,0 a72,72 0 1,1 -144,0"/>
</defs>
<g class="badge-ring">
<text><textPath href="#badge-arc">
NINTH WAVE · EST. 2019 · COLD WATER · NO HEROICS ·
</textPath></text>
</g>
.badge-ring{
transform-box: fill-box; /* rotate about the ring itself */
transform-origin: center;
animation: badge-spin 36s linear infinite;
}
@media (prefers-reduced-motion: reduce){
.badge-ring{ animation: none } /* the stamp just sits there. stamps do that */
}
Because the rotation is pure CSS it costs nothing, works with JavaScript disabled, and switches off cleanly under reduced motion.
brightness(1.06) contrast(.94) saturate(.82) sepia(.1) — overexposed a touch
further than the file itself, on foam-colored paper, rotated −1.1°.feTurbulence data-URI, mix-blend-mode: multiply at low opacity —
warm paper, one HTTP request saved, zero per-frame cost..js
class gates every enhancement.--coral-text, --denim-deep) while decoration keeps the faded ones.Every path is relative and there is no build, so deploying is three commands:
gh repo create ninth-wave-surf --public --source . --push
gh api --method POST /repos/USER/ninth-wave-surf/pages \
-f 'source[branch]=main' -f 'source[path]=/'
# live at https://USER.github.io/ninth-wave-surf/ in ~1 min
A .nojekyll file keeps Pages from touching the folder structure. That's the whole
deploy. Wax not included.
Ninth Wave is a design-showcase concept — the brand, products, prices and surf report are fiction, and the signup form has no backend. See the repository README for the full demo-vs-real map.
← Back to Ninth Wave