Driftline is an ocean-conservation nonprofit concept built as one HTML file, one stylesheet and one script — vanilla JS with GSAP for scroll work. No photos anywhere: the water is drawn, the data is the imagery, and scrolling the page is a dive from the surface to the midnight zone.
Nonprofit sites usually beg. Driftline testifies: every claim on the page is a number that came off a scale, and the design's job is to make data feel like water. Three moves carry the whole site — a wave system that flows across every section boundary, a scroll descent that takes you from sunlight to midnight zone (with a depth gauge reading out the metres), and a donation panel that converts dollars into kilograms in real time. Urgency lives in one coral accent, used maybe three times. Everything else is patient, deep blue.
requestAnimationFrame.Every wave on the page — the hero water and the four section-boundary dividers — is the same
WaveField class pointed at a different canvas. Each band is two summed
sines (one long swell plus one short chop, moving in opposite directions), which is the
cheapest trick there is for water that never visibly repeats:
const y = mid
+ A * .62 * Math.sin(u / L.l1 * TAU + t * L.s1 + seed)
+ A * .38 * Math.sin(u / L.l2 * TAU - t * L.s2 + seed * 1.7);
The band is built as an open Path2D, cloned and closed to fill the water, and the
open copy is stroked with a faint foam line — so crest and body come from one set of points:
const closed = new Path2D(open);
closed.lineTo(w, h); closed.lineTo(0, h); closed.closePath();
ctx.fill(closed); // the water
if (L.stroke) ctx.stroke(open); // the foam crest
One shared rAF loop drives all six canvases, but an IntersectionObserver marks which are on
screen and only those get drawn. Each divider gets a data-seed so no two flow in
phase. With prefers-reduced-motion, the loop never starts — every field is drawn
once at a fixed t and the ocean holds still.
The trick most people would reach for JS to do is pure CSS: the document body's background is one vertical gradient from surface blue to midnight black, so scrolling the page literally is descending through the water column — no JavaScript, no scroll listener, works in every browser ever:
body{
background:linear-gradient(180deg,
#0a2a43 0%, #072138 32%, #051b2e 52%,
#041524 72%, #020a16 100%);
}
JS then adds the instrumentation on top. A fixed mono HUD converts scroll progress into metres and names the zone you're in — the sections are labelled 0 m, 200 m, 400 m … 1000 m to match:
const p = scrollY / (doc.scrollHeight - innerHeight);
const d = Math.round(p * 1000);
hud.textContent = String(d).padStart(4, '0');
zone.textContent = d < 180 ? 'sunlight zone'
: d < 860 ? 'twilight zone' : 'midnight zone';
GSAP's ScrollTrigger scrubs one more layer — a fixed radial "veil" whose opacity rises with depth, so the edges of the viewport dim the deeper you go. If GSAP fails to load, a plain scroll listener does the same job; if motion is reduced, the veil stays off entirely.
The tide chart is a canvas bar chart where the bars flood upward — each bar eases in
with a 110 ms stagger, filled with a teal-to-transparent gradient and capped with a 2px foam
line, so the series reads as a tide coming in year over year. The donation panel runs the same
philosophy in reverse: a real radio group of preset amounts and a one-time/monthly toggle feed
one function that rewrites an aria-live line — "$25 hauls and logs 40 kg" —
so screen readers hear the equivalence exactly when sighted users see it.
input type="radio" in a fieldset (arrow keys work), the frequency toggle is a
second radio group styled as a segmented control, and the custom field checks its radio on
focus. Impact text updates via aria-live="polite"..js class gates every reveal; without
scripts the hero shows a static SVG wave, the dividers fall back to CSS gradients, and every
word is readable.#ff7a59) reserved for the
scariest stat, low spot counts, and nothing else — urgency you ration stays urgent.min(devicePixelRatio, 2) and resize on a 150 ms debounce.The tide remembers everything we give it. The site's only job is to make you believe a dataset can be a love letter.
Every path is relative and there is no build step, so deploying is three commands:
gh repo create driftline-ocean --public --source . --push
gh api --method POST /repos/USER/driftline-ocean/pages \
-f 'source[branch]=main' -f 'source[path]=/'
# live at https://USER.github.io/driftline-ocean/ in ~1 min
A .nojekyll file keeps Pages from processing the folder. That's the whole deploy.
Driftline is a design-showcase concept, not a real nonprofit — the organisation, events and figures are illustrative. See the repository README for the full demo-vs-real map.
← Back to Driftline