← Ninth Wave The guide · how it was built
Design showcase · build notes

A poster that drifts.

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.

The sun-bleached hero photograph: a surfer carrying a longboard along wet sand at noon.
Fig. 09 — the one photograph on the site, overexposed a little further in CSS.

The idea

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.

The stack

Signature technique #1 — the scroll-drift shelf

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.

Signature technique #2 — the rotating stamp badge

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.

Details that matter

Ship it on GitHub Pages

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