Base's WebGL bar-field hero
Tiny audio waveforms - WebGL fragment shader.
Base's homepage hero is a white canvas scattered with little clusters of thin vertical bars - like tiny audio waveforms - that drift around and gather under your cursor. Look closely and each bar has a faint colour fringe: a periwinkle core edged with pale green, cream, and lavender, as if the image were slightly out of register. The whole thing is one WebGL <canvas> running a single fragment shader. Here's how to rebuild it.
Move your cursor across the demo - the bars cluster toward it - and hit Freeze the field to stop time.
WebGL bar-field hero
The blockchain
for global finance.
Built on a single WebGL fragment shader.
One triangle, one shader
A GPU draws triangles, so the trick to a full-screen effect is to hand it a single triangle big enough to cover the whole viewport and let the fragment shader - a tiny program that runs once per pixel - decide the colour of everything inside it. One oversized triangle is cheaper than a quad (no shared edge, no overdraw):
// clip space goes -1..1, so this triangle's corners
// at (-1,-1) (3,-1) (-1,3) spill past every edge
gl.bufferData(gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 3, -1, -1, 3]),
gl.STATIC_DRAW);
// the vertex shader forwards a 0..1 uv; we feed in u_time,
// u_resolution and u_pointer as uniforms each frameColumns of rounded bars
The building block is a vertical bar, repeated in columns. fract makes the columns for free: divide x by a spacing and floor tells you which column you're in, while the centre of that column is all you need to place a bar. A capsule SDF (a rounded rectangle) draws the bar itself with soft, rounded caps:
float colW = 0.024, BW = 0.0045; // column spacing + bar half-width
float colIndex = floor(sp.x / colW);
float cx = (colIndex + 0.5) * colW; // this column's centre
float bar(vec2 q, float hh) { // rounded vertical bar, half-height hh
vec2 d = abs(q) - vec2(BW, hh);
float sdf = min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - BW;
return smoothstep(0.0025, -0.0025, sdf);
}Gather them into clusters that chase the cursor
A bar's height is what makes the waveform shape. Each column gets an amplitude from a set of cluster centres - a Gaussian of the horizontal distance, so columns near a centre stand tall and fade out to the sides. One centre rides the cursor; a few others drift on sin paths. Multiply by a per-column random value and the tops turn ragged, reading as separate bars rather than a solid blob:
float dx = cx - centre[i].x;
float amp = strength[i] * exp(-(dx * dx) / (2.0 * sigX * sigX));
amp *= 0.45 + 0.55 * hash(colIndex); // ragged per-column height
float hh = amp * MAXH;
float m = bar(vec2(sp.x - cx, sp.y - centre[i].y), hh); // take the max over centresThe cursor centre gets full strength only while the pointer is over the canvas, so the field sits calm until you move into it, then a tall cluster blooms under the cursor.
Fake the chromatic aberration
The colour fringe is the signature, and it's just the same bar drawn a few times at tiny vertical offsets, each tinted differently, then stacked. Paint the outer offsets first and the periwinkle core last: where everything overlaps you get blue, and where the shifted copies peek out past the core you get green and cream at the caps and a thin lavender edge. The fringe colours are pale on purpose - these are sampled straight off the real hero - and a white canvas is what lets them show at all:
float core = bar(q, hh);
float green = max(bar(q - vec2(0.0, e)), bar(q + vec2(0.0, e)));
float cream = max(bar(q - vec2(0.0, e * 1.6)), bar(q + vec2(0.0, e * 1.6)));
vec3 col = vec3(1.0); // white page
col = mix(col, vec3(0.96, 0.90, 0.68), cream); // #f6e5ae cream tips
col = mix(col, vec3(0.80, 0.94, 0.67), green); // #ccefac green edges
col = mix(col, vec3(0.45, 0.45, 0.97), core); // #7373f7 periwinkle core on topThat's the part I kept getting wrong from a screenshot alone - the effect isn't dots or a smooth gradient, it's registered copies of a bar field. One more touch makes it sit right: each bar's opacity fades with its cluster envelope (op = clamp(env * 1.7, 0.0, 1.0)), so bars go translucent toward the edges instead of cutting off hard.
Shimmer, and respecting motion
A slow sin term added to each column's amplitude makes the bars shimmer gently in place, on top of the clusters drifting. And the one non-negotiable: honour prefers-reduced-motion. The render loop keeps running so the cursor still gathers a cluster, but we stop advancing the clock - the field freezes into a still composition instead of a moving one:
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
// ...inside the rAF loop:
if (!paused && !reduced) clock += dt; // freeze time, keep drawingThat's the whole recipe: one triangle, a fract grid of capsule bars, cluster amplitudes that follow the cursor, and a few offset tinted copies for the chromatic fringe. A surprisingly intricate hero that would be a heavy video becomes a few hundred bytes of GLSL - sharp at any resolution, interactive, and running entirely on the GPU.
Newsletter
Stay updated with my latest articles and projects. No spam, no nonsense.