IrisBackgroundsSpectrum Glass
Spectrum Glass
The full hue wheel run as vertical bars behind fluted glass, each seam weaving up and down into a diamond lattice — a press on the glass sends a ripple bending the weave outward.
Spectrum Glass
The full hue wheel — blue, teal, green, gold, orange, red, magenta, back to blue — repeats a few times left to right in vertical bars, each seam weaving up and down rather than running straight. Where two neighbouring bars' seams pinch together they read as a thin bright line; where they swell apart they pool into a soft lens of blended colour — the diamond lattice the reference photo carries, built from that weave rather than a printed texture.
The pointer presses into the glass: a radial ripple bends the weave outward from the cursor like a hand on a soft pane, the bars briefly sharpen and lift contrast inside that reach, and a thin bright fringe rides the ripple's leading edge the way light catches a moving bulge in real glass. Left alone, the weave still drifts on its own, slowly, so it never sits as a static swatch.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { SpectrumGlassField } from "./SpectrumGlassField";
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">
<SpectrumGlassField />
</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 field of vertical rainbow bars, seen as if through a sheet of
* fluted glass and dragged with a long vertical exposure: the full hue wheel
* — blue, teal, green, gold, orange, red, magenta, back to blue — repeats a
* few times left to right, each bar's edge weaving up and down rather than
* running straight, so neighbouring bars pinch into thin bright seams at
* some heights and swell into soft lens-shaped pools of blended colour at
* others. That weave, not a texture, is what reads as the reference photo's
* diamond lattice.
*
* Interactive: the pointer presses into the glass — a radial ripple bends
* the weave outward from the cursor like a hand on a soft pane, the bars
* briefly sharpen and lift contrast inside that reach, and a soft white
* fringe rides the ripple's leading edge the way light catches a moving
* bulge in real glass. Move away and it settles back to a slow, idle drift.
*
* One of the reusable background fields (`PrismField`, `GauzeField`,
* `MeshFluidField`, …). 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-spectrumglassfield__floor` underneath visible — a still frame in
* the same palette, never a blank box.
*
* Like `PrismField`, the palette is a fixed set of uniforms rather than the
* accent ramp — this full-saturation rainbow lives nowhere in the site's own
* (dark-mode, single-hue-family) ramp.
*
* Reading guard: when `guardSelector` resolves to an element, the field
* measures that block every frame and clamps its own luminance under a
* ceiling inside that region (hue and saturation untouched). `null` (the
* default) turns the guard off — for decorative use where nothing sits on
* top.
*/
/* Palette, sRGB 0–1. Uniforms, not tokens — see the note above. One
continuous hue wheel, blue back to blue, through six named stops between —
same shape as PrismField's `sweep`, saturated rather than pastel. */
const PALETTE: Record<string, [number, number, number]> = {
u_blue: [0.11, 0.22, 0.62], // where the wheel closes
u_teal: [0.07, 0.52, 0.56],
u_green: [0.22, 0.66, 0.34],
u_gold: [0.93, 0.82, 0.28],
u_orange: [0.94, 0.5, 0.14],
u_red: [0.82, 0.2, 0.19],
u_purple: [0.5, 0.17, 0.56],
u_indigo: [0.16, 0.16, 0.58], // eases back toward blue
};
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_blue;
uniform vec3 u_teal;
uniform vec3 u_green;
uniform vec3 u_gold;
uniform vec3 u_orange;
uniform vec3 u_red;
uniform vec3 u_purple;
uniform vec3 u_indigo;
uniform vec4 u_readA;
uniform float u_guard;
const float REPEATS = 8.5; /* how many times the wheel cycles left→right */
/* The wheel itself: blue → teal → green → gold → orange → red → purple →
indigo → blue, each transition its own soft smoothstep so no seam reads
as a hard swatch edge. */
vec3 wheel(float t) {
t = fract(t);
vec3 col = mix(u_blue, u_teal, smoothstep(0.0, 0.16, t));
col = mix(col, u_green, smoothstep(0.12, 0.30, t));
col = mix(col, u_gold, smoothstep(0.28, 0.46, t));
col = mix(col, u_orange, smoothstep(0.44, 0.58, t));
col = mix(col, u_red, smoothstep(0.56, 0.70, t));
col = mix(col, u_purple, smoothstep(0.68, 0.84, t));
col = mix(col, u_indigo, smoothstep(0.82, 0.94, t));
col = mix(col, u_blue, smoothstep(0.92, 1.0, t));
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 / max(res.y, 1.0);
vec2 P = (uv - 0.5) * vec2(aspect, 1.0);
vec2 ptr = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
vec2 toP = P - ptr;
float dP = length(toP);
/* the press: a radial ripple bending outward from the cursor, and a wide
soft reach where the weave sharpens, both keyed off presence so they
fade the instant the pointer leaves */
float ripplePhase = dP * 26.0 - u_time * 2.6;
float rippleEnv = exp(-dP * dP * 5.0) * u_pointer.z;
float ripple = sin(ripplePhase) * rippleEnv;
float focus = u_pointer.z * exp(-dP * dP * 2.2);
float drift = u_time * 0.012;
float barWidth = 1.0 / REPEATS;
/* the woven boundary: two sine octaves whose phase itself rotates with
uv.x (the "+ uv.x * K" term), not just time — so the wave is not one
shared ripple every bar swims in together, but a phase that has turned
most of a full turn by the time you cross a single bar's width. That is
what lets one bar pinch to a thin bright seam while its neighbour, a
turn further round the phase, is swelling into a soft pool at the same
height — the diamond lattice, not a synchronised sway. The ripple adds
its own bend on top, radial rather than vertical, so a press reads as a
bulge in the glass rather than a shifted stripe. */
float weave =
barWidth * 0.11 * sin(uv.y * 8.0 + uv.x * 6.0 + drift * 4.0) +
barWidth * 0.07 * sin(uv.y * 13.0 - uv.x * 10.0 - drift * 6.0 + 1.7) +
barWidth * 0.3 * ripple;
float jitterAmt = mix(barWidth * 0.05, barWidth * 0.015, focus);
float grain = fbm(vec2(uv.x * 70.0 + drift * 2.0, uv.y * 4.0));
float xWarp = uv.x + weave + grain * jitterAmt;
float t = xWarp * REPEATS + drift * 0.5;
vec3 col = wheel(t);
/* fine per-bar grain, quieting inside the focus reach where things
sharpen rather than shimmer */
float fine = fbm(vec2(xWarp * 90.0, uv.y * 3.0 + drift * 3.0));
col *= 1.0 + fine * mix(0.1, 0.03, focus);
/* pulled into focus: contrast lifts and the seam sharpens, as if the
glass itself had gone briefly taut under the touch */
col = mix(col, (col - 0.5) * 1.22 + 0.5, focus * 0.5);
/* soft top/bottom vignette, echoing the reference photo's darker corners */
float vig = smoothstep(1.0, 0.35, abs(uv.y - 0.5) * 2.0);
col *= mix(0.72, 1.0, vig);
/* the ripple's own leading edge catching the light, a thin bright fringe
riding outward from the touch */
float fringe = clamp(ripple, 0.0, 1.0) * rippleEnv;
col += vec3(fringe * 0.35);
col += vec3(1.0) * focus * 0.05;
col = max(col, 0.0);
/* ---- the reading guard (see TileField for the full rationale) ---- */
vec2 rd = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
float md = mix(max(rd.x, rd.y), length(rd), 0.4);
float guardBand = 1.0 - smoothstep(0.72, 2.1, md);
col = mix(col, holdUnder(col, 0.14), guardBand * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
const PALETTE_UNIFORMS = [
"u_blue",
"u_teal",
"u_green",
"u_gold",
"u_orange",
"u_red",
"u_purple",
"u_indigo",
] as const;
export interface SpectrumGlassFieldProps {
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 SpectrumGlassField({
className,
guardSelector = null,
}: SpectrumGlassFieldProps) {
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: [...PALETTE_UNIFORMS, "u_readA", "u_guard"],
onInit: (gl, u) => {
for (const name of PALETTE_UNIFORMS) {
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 PrismField / GauzeField: once painted,
the last frame stays on screen while the surface is parked
off-view, so scrolling away and back does not crossfade to the
static floor. A lost context still clears it below. */
onLost: () => canvas.removeAttribute("data-shader"),
maxPixels: 2_200_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(_120%_90%_at_50%_0%,transparent_60%,oklch(0.04_0.01_264_/_0.5)_100%_),radial-gradient(_120%_90%_at_50%_100%,transparent_60%,oklch(0.04_0.01_264_/_0.5)_100%_),repeating-linear-gradient(_90deg,oklch(0.32_0.14_264)_0%,oklch(0.52_0.13_200)_8%,oklch(0.58_0.16_145)_16%,oklch(0.86_0.16_95)_24%,oklch(0.68_0.19_45)_32%,oklch(0.5_0.18_25)_40%,oklch(0.42_0.17_325)_48%,oklch(0.28_0.12_275)_56%,oklch(0.32_0.14_264)_64%_)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(_90deg,oklch(0.05_0.01_264_/_0.14)_0_2px,transparent_2px_7px,oklch(1_0_0_/_0.06)_7px_9px,transparent_9px_15px_)]" />
<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