Skip to content

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.

prism

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.

  • multi-hue
  • iridescent
  • pointer-driven
  • glass
Family
prism
Status
Available
Licence
Free

Install

No installation needed — self-contained, paste-in code.

Usage

Drop it straight into a page.

example.tsx
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>
  );
}

More backgrounds

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

Fardin Omor Afnan

Reads and answers every request himself

Or start with a section.

Whole page sections, composed and ready to drop in — take one and build the rest of the page around it.

Browse sections