IrisBackgroundsColonnade
Colonnade
Two mirrored banks of dark pillars framing an empty centre, each lit only along its top edge in hot ember orange.
Colonnade
Built to one reference: four pillars hug each side of the frame, and the centre stays empty for a headline and a form. Each pillar catches light only at its top, as a thin white-hot line with a glow above and a warm wash spilling down its face. Above that line, only the pillar's two vertical edges glow, fading out toward the top of the frame. The tops step down toward the centre, so the two banks read as staircases pointing at the copy.
Pillars are sized in height units and anchored to their own side, so the banks keep their proportions at any aspect ratio and only the empty centre stretches. The pointer is a counterweight: the pillar under the cursor reaches for its height, its neighbours follow at a fraction, and its mirrored twin on the far bank moves the opposite way by half as much, as if both banks hung from one balance beam. Every top rides its own damped spring, so it overshoots a touch before settling and glows hotter while it moves. Move the cursor into the empty centre and the tops settle back to the reference staircase.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { ColonnadeField } from "./ColonnadeField";
export default function Example() {
return (
// Fills its nearest positioned ancestor (it renders itself `absolute
// inset-0`) — give it a sized, relatively positioned box.
<div className="relative isolate h-[32rem] w-full overflow-hidden rounded-2xl">
<ColonnadeField />
</div>
);
}Component
The real source, exactly as it ships — multiple files, kept together.
"use client";
import { useEffect, useRef } from "react";
import { mountShaderSurface } from "@/lib/shader-surface";
/**
* Two mirrored banks of four dark pillars, one bank hugging each side of
* the frame, with the centre left empty for a headline and a form. Each
* pillar is lit only along its own top edge — a hot orange cap line that
* bleeds a warm wash down the pillar's face — and above the cap, only the
* pillar's two vertical edges catch the light, fading out toward the top of
* the frame. The caps step: from the outside in, the second pillar stands
* tallest and the innermost sits lowest, so the four caps read as a
* staircase dropping toward the centre.
*
* Every pillar is placed in height units (not uv), anchored to its own side
* of the frame, so the banks keep their proportions at any aspect ratio and
* the empty centre is what stretches.
*
* The pointer is a counterweight. The pillar under the cursor reaches for
* the cursor's height (up or down), its neighbours follow at a fraction,
* and its mirrored twin on the far bank moves the opposite way by half as
* much — the two banks behave like one balance beam. Every cap rides its
* own damped spring, so it overshoots a touch and settles, and it runs
* hotter while it's moving. With the pointer gone (or in the empty centre)
* the caps settle back to the reference staircase and just breathe.
*
* One of the reusable background fields. Drop it into any `position:
* relative`/`isolate` parent — it fills the box. Built on
* `lib/shader-surface.ts`, so every degradation path is already handled: no
* WebGL, a blocked or lost context, a hidden tab, or `prefers-reduced-motion`
* all leave the CSS floor underneath visible.
*/
/* Palette, sRGB 0–1. Uniforms, not tokens — a fixed ember mood built to the
one reference. */
const PALETTE: Record<string, [number, number, number]> = {
u_ground: [0.024, 0.02, 0.018], // the empty centre, near-black with a warm cast
u_pillar: [0.05, 0.036, 0.028], // a pillar's own body above its cap
u_ember: [1.0, 0.36, 0.06], // the glow — cap bloom, face wash, edge lines
u_hot: [1.0, 0.78, 0.5], // the cap's white-hot core
};
const FRAG = `
uniform vec2 u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3 u_ground;
uniform vec3 u_pillar;
uniform vec3 u_ember;
uniform vec3 u_hot;
/* Live cap heights (uv y) and heat (0..1) for each bank's four pillars,
outermost first — driven by the spring simulation on the CPU. */
uniform vec4 u_capL;
uniform vec4 u_capR;
uniform vec4 u_heatL;
uniform vec4 u_heatR;
float pick(vec4 v, float i) {
return i < 0.5 ? v.x : i < 1.5 ? v.y : i < 2.5 ? v.z : v.w;
}
/* One pillar's light at P (height units, x measured inward from the
bank's own frame edge, y up from the bottom). */
vec3 pillar(vec2 P, float x0, float x1, float cy, float heat, float phase, float H) {
float w = x1 - x0;
float inX = step(x0, P.x) * step(P.x, x1);
float breathe = (0.9 + 0.1 * sin(u_time * 0.55 + phase)) * (1.0 + heat * 0.9);
vec3 col = vec3(0.0);
/* horizontal distance to the pillar's span, 0 inside */
float dxSpan = max(max(x0 - P.x, P.x - x1), 0.0);
float dy = P.y - cy;
/* ---- the cap: a thin hot line with a tight bloom above and a longer
one below, both clipped softly past the pillar's two ends ---- */
float px = 1.0 / H; /* one css pixel, in height units */
float ends = exp(-dxSpan * dxSpan / (0.00002));
float core = exp(-dy * dy / (2.2 * px * px)) * ends;
float bloomUp = exp(-max(dy, 0.0) / 0.016) * step(0.0, dy);
float bloomDn = exp(-max(-dy, 0.0) / 0.02) * step(dy, 0.0);
float bloom = (bloomUp * 0.8 + bloomDn * 0.8) * exp(-dxSpan / 0.006);
col += u_hot * core * 1.1 * breathe;
col += u_ember * bloom * 0.75 * breathe;
/* ---- the face below the cap: a warm wash falling off downward, a touch
brighter along the pillar's two edges ---- */
float below = step(dy, 0.0) * inX;
float fall = exp(dy / 0.11);
float u = (P.x - x0) / w;
float rim = pow(abs(u - 0.5) * 2.0, 6.0);
col += u_ember * below * fall * (0.26 + 0.24 * rim) * breathe;
col += u_ember * below * exp(dy / 0.4) * 0.035;
/* ---- above the cap: only the two vertical edges catch light, thin
lines with a soft halo, dying out toward the top of the frame ---- */
float above = smoothstep(-0.002, 0.004, dy);
float rise = exp(-max(dy, 0.0) / 0.24) * smoothstep(0.98, 0.72, P.y);
float dL = abs(P.x - x0);
float dR = abs(P.x - x1);
float edge = exp(-dL * dL / (1.6 * px * px)) + exp(-dR * dR / (1.6 * px * px));
float halo = exp(-dL / 0.007) + exp(-dR / 0.007);
col += u_ember * above * rise * (edge * 0.3 + halo * 0.16) * breathe;
return col;
}
void main() {
vec2 res = u_res / u_scale;
vec2 uv = gl_FragCoord.xy / u_scale / res; /* 0..1, y up */
float aspect = res.x / res.y;
/* mirror: both banks are the same bank, measured inward from their own
frame edge, in height units */
float X = min(uv.x, 1.0 - uv.x) * aspect;
vec2 P = vec2(X, uv.y);
/* pillar width — the reference's 7.1% of frame width at its own 1.44
aspect, held in height units, and eased down on narrow frames so the
two banks never meet */
float W = min(0.102, aspect * 0.1);
vec3 col = u_ground;
/* the pillar bodies above their caps sit a hair lighter than the empty
centre, with a faint vertical gradient toward the top */
float right = step(0.5, uv.x);
vec4 caps = mix(u_capL, u_capR, right);
vec4 heats = mix(u_heatL, u_heatR, right);
float bank = step(X, W * 4.0);
float cyHere = pick(caps, floor(X / W));
float bodyLift = bank * smoothstep(cyHere - 0.01, cyHere + 0.02, uv.y) * mix(1.0, 0.55, uv.y);
col = mix(col, u_pillar, bodyLift);
for (int i = 0; i < 4; i++) {
float fi = float(i);
col += pillar(P, fi * W, (fi + 1.0) * W, pick(caps, fi), pick(heats, fi), fi * 1.7 + right * 2.3, res.y);
}
/* a soft falloff at the very top, where the nav sits */
col *= mix(1.0, 0.7, smoothstep(0.85, 1.0, uv.y));
col = max(col, 0.0);
col = col / (1.0 + max(max(col.r, col.g), col.b) * 0.18); /* gentle roll-off so the cores don't clip flat */
col += (bayer8(gl_FragCoord.xy) - 0.5) * (1.6 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
/* The reference staircase — each bank's cap heights at rest, outermost
first, uv y measured off the reference. */
const REST = [0.282, 0.417, 0.184, 0.063];
/* Pillar width in height units — must match `W` in the shader. */
const pillarWidth = (aspect: number) => Math.min(0.102, aspect * 0.1);
export interface ColonnadeFieldProps {
className?: string;
}
export function ColonnadeField({ className }: ColonnadeFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
/* Eight damped springs, left bank then right, outermost first. */
const pos = [...REST, ...REST];
const vel = new Array<number>(8).fill(0);
const heat = new Array<number>(8).fill(0);
let lastTime = -1;
return mountShaderSurface(canvas, {
fragment: FRAG,
uniforms: [...Object.keys(PALETTE), "u_capL", "u_capR", "u_heatL", "u_heatR"],
onInit: (gl, u) => {
for (const name of Object.keys(PALETTE)) {
if (u[name]) gl.uniform3fv(u[name], PALETTE[name]);
}
},
onFrame: (gl, u, s) => {
const dt = lastTime < 0 ? 0 : Math.min(Math.max(s.time - lastTime, 0), 1 / 20);
lastTime = s.time;
const { x, y, presence } = s.pointer;
const aspect = s.rect.width / Math.max(s.rect.height, 1);
const W = pillarWidth(aspect);
const side = x < 0.5 ? 0 : 1;
/* the pointer's position in pillar units, measured inward from its
own bank's frame edge — 0.5 is the centre of the outermost */
const f = (Math.min(x, 1 - x) * aspect) / W - 0.5;
/* fades out as the pointer leaves the bank for the empty centre */
const inBank = presence * Math.min(Math.max((4.6 - f) / 1.1, 0), 1);
const reach = Math.min(Math.max(y, 0.04), 0.86);
const target = [...REST, ...REST];
for (let i = 0; i < 4; i++) {
const w = inBank * Math.exp(-((i - f) ** 2) / 0.45);
const d = (reach - REST[i]) * 0.6 * w;
target[side * 4 + i] = REST[i] + d;
target[(1 - side) * 4 + i] = Math.max(REST[i] - d * 0.5, 0.02);
}
for (let k = 0; k < 8; k++) {
vel[k] += ((target[k] - pos[k]) * 95 - vel[k] * 11) * dt;
pos[k] += vel[k] * dt;
const want = Math.min(Math.abs(vel[k]) * 1.6, 1);
heat[k] += (want - heat[k]) * Math.min(dt * 8, 1);
}
if (u.u_capL) gl.uniform4f(u.u_capL, pos[0], pos[1], pos[2], pos[3]);
if (u.u_capR) gl.uniform4f(u.u_capR, pos[4], pos[5], pos[6], pos[7]);
if (u.u_heatL) gl.uniform4f(u.u_heatL, heat[0], heat[1], heat[2], heat[3]);
if (u.u_heatR) gl.uniform4f(u.u_heatR, heat[4], heat[5], heat[6], heat[7]);
},
onPainted: () => canvas.setAttribute("data-shader", "on"),
onLost: () => canvas.removeAttribute("data-shader"),
maxPixels: 1_300_000,
dprCap: 1.5,
});
}, []);
return (
<div
className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
aria-hidden="true"
>
<div className="absolute inset-0 [background:linear-gradient(to_top,oklch(0.36_0.12_45_/_0.35),transparent_28%),linear-gradient(to_right,oklch(0.14_0.012_45)_0_28%,oklch(0.1_0.004_50)_28%_72%,oklch(0.14_0.012_45)_72%)]" />
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full opacity-0 transition-opacity duration-[--duration-slow] ease-[--ease-standard] data-[shader=on]:opacity-100" />
</div>
);
}Custom work
Need one that isn't in the catalogue?
Describe what you're after and I'll reply by email — no promise on turnaround yet, this is a new channel, not a service with a set price or queue.
Fardin Omor Afnan
Reads and answers every request himself