IrisBackgroundsReeded Glass
Reeded Glass
A panel of ribbed glass with warm backlight pooling behind it, each rib bending the glow a little as it passes through.
Reeded Glass
Vertical half-round ribs run the full height of the frame, each one a true convex curve rather than a flat stripe, catching its own crest of specular light and its own seam of shadow at the edge. Behind the glass sits a bank of backlight — soft amber pools drifting slowly, one cooler grey-blue cast further along — and every rib refracts that light a little sideways as it passes through, the way a real glass rod bends a point of light off its own axis, so the pools smear gently from column to column instead of lining up as a flat gradient. A dark vignette caps the top and bottom, holding the brightest wash through the middle band.
The cursor is a hand on the light behind the glass: move it and the key light tilts to follow, leaning the specular run along each rib's crest toward wherever the visitor is pointing, while a warm pool of its own drops in at the cursor's position. Move away and the light settles back into a slow, idle sway rather than sitting still.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { ReedField } from "./ReedField";
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">
<ReedField />
</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";
/**
* A live WebGL panel of reeded glass — vertical half-round ribs, each one a
* true convex normal rather than a flat stripe, sitting in front of a bank
* of warm and cool backlight pools. The ribs don't just shade: each one
* refracts what sits behind it a little sideways, the way a real rod of
* glass bends the light passing through its curve, so the pools smear
* softly from column to column instead of lining up as a flat gradient.
*
* The backlight is a light source, and the pointer is a hand moving it: the
* cursor both drops a warm pool of its own into the field and tilts the key
* light's angle (x steers yaw, y steers pitch), so the specular run down
* each rib's crest visibly leans toward wherever the visitor is pointing. A
* slow idle sway keeps the same light drifting on its own with no cursor
* present, and a dark vignette caps the top and bottom the way the source
* photograph does, leaving the brightest band through the middle third.
*
* 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 `.iris-reedfield__floor`
* underneath visible.
*
* Like `CoilField` and `CascadeField`, the palette is a fixed set of five
* colours passed as uniforms rather than read from the site's dark ramp —
* this warm bronze-and-cream mood is its own, not the portfolio's.
*
* Reading guard: when `guardSelector` resolves to an element, the field
* measures that block every frame and clamps its own luminance under a
* ceiling in that region (hue and saturation untouched). `null` (the
* default) turns the guard off — for decorative use where nothing sits on
* top of it.
*/
/* Palette, sRGB 0–1. Uniforms, not tokens — see the note above. */
const PALETTE: Record<string, [number, number, number]> = {
u_dark: [0.035, 0.03, 0.026], // the vignette caps, and each rib's own shadow
u_mid: [0.32, 0.26, 0.21], // the ribs' bronze body between the pools
u_warm: [0.62, 0.42, 0.24], // amber backlight, the wide pools
u_cream: [0.97, 0.88, 0.72], // hot core of a pool, and the specular crest
u_cool: [0.4, 0.44, 0.5], // one grey-blue pool, a stray cast further along
};
const FRAG = `
uniform vec2 u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3 u_pointer; /* x, y (0 bottom .. 1 top), presence 0..1 */
uniform vec3 u_dark;
uniform vec3 u_mid;
uniform vec3 u_warm;
uniform vec3 u_cream;
uniform vec3 u_cool;
uniform vec4 u_readA;
uniform float u_guard;
/* a soft round pool of backlight, in aspect-corrected units */
float pool(vec2 p, vec2 c, float r) {
float d2 = dot(p - c, p - c);
return exp(-d2 / (r * r));
}
/* the bank of light behind the glass: a few slow-drifting warm and cool
pools plus the pointer's own, composited over a near-black ground */
vec3 lightField(vec2 p, float t, vec2 pc, float pres) {
vec2 c1 = vec2(-0.30, 0.16) + 0.05 * vec2(sin(t * 0.045), cos(t * 0.038));
vec2 c2 = vec2(0.12, -0.12) + 0.06 * vec2(cos(t * 0.031), sin(t * 0.05));
vec2 c3 = vec2(0.42, 0.24) + 0.04 * vec2(sin(t * 0.04 + 1.9), cos(t * 0.026));
vec2 c4 = vec2(-0.06, -0.32);
float g1 = pool(p, c1, 0.30);
float g2 = pool(p, c2, 0.24);
float g3 = pool(p, c3, 0.20);
float g4 = pool(p, c4, 0.36);
float gp = pool(p, pc, 0.26) * pres;
vec3 col = u_dark;
col = mix(col, u_mid, clamp(g1 * 1.1 + g2 * 0.7 + g3 * 0.7 + g4 * 0.6 + gp * 0.9, 0.0, 1.0));
col += u_warm * (g1 * 0.85 + g4 * 0.5 + gp * 0.55);
col += u_cool * g3 * 0.55;
col += u_cream * pow(g1, 1.6) * 0.55;
col += u_cream * pow(g2, 1.8) * 0.35;
col += u_cream * pow(gp, 1.4) * 0.7;
return col;
}
void main() {
vec2 res = u_res / u_scale;
vec2 fc = gl_FragCoord.xy / u_scale;
vec2 uv = fc / res; /* 0..1, y up */
vec2 p = (fc - 0.5 * res) / res.y;
float aspect = res.x / res.y;
float pres = u_pointer.z;
vec2 ptrN = (u_pointer.xy - 0.5) * 2.0; /* -1..1 */
vec2 pc = (u_pointer.xy - 0.5) * vec2(aspect, 1.0) * 1.7;
float t = u_time;
/* the corrugation: half-round convex ribs, a fixed count per unit height
so the rib width holds steady across aspect ratios */
float ribDensity = 15.0;
float rx = p.x * ribDensity + 0.5;
float ribId = floor(rx);
float localX = fract(rx) - 0.5; /* -0.5..0.5 across one rib */
float nx = clamp(localX * 2.15, -1.0, 1.0);
float nz = sqrt(max(1.0 - nx * nx, 0.0));
float jit = dotHash(vec2(ribId, 3.7)) - 0.5;
vec3 ribNormal = normalize(vec3(nx + jit * 0.06, 0.0, nz));
/* the rib bends what sits behind it a little sideways, same idea as a
real glass rod refracting a point of light off-axis */
float refr = 0.10;
vec2 bgP = p + vec2(ribNormal.x * refr, jit * 0.01);
vec3 bg = lightField(bgP, t, pc, pres);
/* the key light sways on its own, and leans toward the pointer the way a
hand tilting the source behind the glass would */
float idleYaw = sin(t * 0.05) * 0.18;
float yaw = idleYaw * (1.0 - pres) + ptrN.x * 0.5 * pres;
float pitch = 0.5 + 0.12 * cos(t * 0.04) * (1.0 - pres) - ptrN.y * 0.22 * pres;
vec3 lightDir = normalize(vec3(-0.4 + yaw, pitch, 0.78));
float diff = max(dot(ribNormal, lightDir), 0.0);
vec3 h = normalize(lightDir + vec3(0.0, 0.0, 1.0));
float spec = pow(max(dot(ribNormal, h), 0.0), 34.0);
float lum = dot(bg, vec3(0.2126, 0.7152, 0.0722));
vec3 col = bg * (0.5 + 0.7 * diff);
col += vec3(1.0) * spec * (0.4 + lum * 0.7);
col += u_cream * spec * 0.25 * (0.3 + pres * 0.5);
/* a seam of shadow at each rib's edge, so neighbours read as separate
rods rather than one continuous ripple */
float seam = smoothstep(0.0, 0.05, 0.5 - abs(localX));
col *= mix(0.62, 1.0, seam);
/* a faint sheen riding each rib's crest, drifting slowly down the panel */
float crest = smoothstep(0.42, 0.5, nz);
float sheen = crest * (0.5 + 0.5 * sin(t * 0.15 - p.y * 2.0));
col += u_cream * sheen * 0.05 * diff;
/* the vignette: dark caps top and bottom, the source photograph's own
brightest band held through the middle */
float vig = smoothstep(0.0, 0.26, uv.y) * smoothstep(0.0, 0.3, 1.0 - uv.y);
col *= mix(0.3, 1.0, vig);
/* ---- the reading guard (see TileField for the full rationale) ---- */
vec2 rd2 = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
float m = mix(max(rd2.x, rd2.y), length(rd2), 0.4);
float band = 1.0 - smoothstep(0.72, 2.1, m);
col = mix(col, holdUnder(col, 0.09), band * u_guard);
col = max(col, 0.0);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface ReedFieldProps {
className?: string;
/**
* CSS selector for the block the reading guard should keep readable,
* resolved against `document`. `null` (the default) turns the guard off.
*/
guardSelector?: string | null;
}
export function ReedField({ className, guardSelector = null }: ReedFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const guardOn = guardSelector != null;
let guardEl: Element | null | undefined;
const readGuardEl = () => {
if (guardEl === undefined) {
guardEl = guardOn ? document.querySelector(guardSelector as string) : null;
}
return guardEl;
};
return mountShaderSurface(canvas, {
fragment: FRAG,
uniforms: [...Object.keys(PALETTE), "u_readA", "u_guard"],
onInit: (gl, u) => {
for (const name of Object.keys(PALETTE)) {
if (u[name]) gl.uniform3fv(u[name], PALETTE[name]);
}
if (u.u_readA) gl.uniform4f(u.u_readA, 0.5, 0.5, 0.44, 0.32);
if (u.u_guard) gl.uniform1f(u.u_guard, guardOn ? 1 : 0);
},
onFrame: (gl, u, s) => {
if (!guardOn || !u.u_readA) return;
let cx = 0.5, cy = 0.5, hw = 0.44, hh = 0.32;
const el = readGuardEl();
const { rect } = s;
if (el && rect.width > 0 && rect.height > 0) {
const r = el.getBoundingClientRect();
const padX = rect.width * 0.09;
const padY = rect.height * 0.11;
cx = (r.left + r.width / 2 - rect.left) / rect.width;
cy = 1 - (r.top + r.height / 2 - rect.top) / rect.height;
hw = (r.width / 2 + padX) / rect.width;
hh = (r.height / 2 + padY) / rect.height;
}
gl.uniform4f(u.u_readA, cx, cy, hw, hh);
},
onPainted: () => canvas.setAttribute("data-shader", "on"),
/* No onIdle — same contract as CoilField / CascadeField: once painted,
the last frame stays on screen while the surface is parked
off-view. onLost drops back to the CSS floor. */
onLost: () => canvas.removeAttribute("data-shader"),
maxPixels: 1_800_000,
dprCap: 1.5,
});
}, [guardSelector]);
return (
<div className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`} aria-hidden="true">
<div className="absolute inset-0 [background:radial-gradient(_46%_40%_at_28%_42%,oklch(0.78_0.09_75_/_0.85)_0%,transparent_68%_),radial-gradient(_40%_34%_at_62%_58%,oklch(0.7_0.05_250_/_0.4)_0%,transparent_70%_),radial-gradient(_50%_42%_at_78%_30%,oklch(0.74_0.1_70_/_0.7)_0%,transparent_68%_),oklch(0.28_0.03_60)] before:content-[''] before:absolute before:inset-0 before:[background:repeating-linear-gradient(_90deg,oklch(0.12_0.015_60_/_0.5)_0%_3%,transparent_3%_9%,oklch(1_0_0_/_0.16)_9%_11%,transparent_11%_15%_)] after:content-[''] after:absolute after:inset-0 after:[background:linear-gradient(_180deg,oklch(0.05_0.01_60_/_0.92)_0%,transparent_26%,transparent_74%,oklch(0.05_0.01_60_/_0.85)_100%_)]" />
<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