Skip to content

IrisBackgroundsGilt Panel

Gilt Panel

A pressed gold sheet — flat saturated pans seamed by fine grooves, lit by a key light you hold and move with the pointer.

rib

Gilt Panel

Built to one specific reference photo of corrugated gold metal siding: a box-rib profile, mostly flat saturated pans rather than an even sine wave, each seamed by one narrow pressed groove with its own true bevel normal. A faint crown rides across every pan so the sheet reads as gently rolled metal instead of a flat card, and a hard near-white run catches the lit lip of whichever seams face the light.

The pointer is the key light, held in the visitor's hand: its x steers which seams catch the highlight, its y tilts the rake flatter or steeper across the panel, and it drops its own warm bloom directly under the cursor — the sheet catching a light brought close, not just a rake sliding past. With no pointer the same light drifts on its own, so a still frame still reads as struck metal.

  • warm
  • pointer-driven
  • metal
Family
rib
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

example.tsx
import { GiltField } from "./GiltField";

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">
      <GiltField />
    </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 pressed golden metal — a box-rib profile, mostly
 * flat gold pans each seamed by one narrow pressed groove, rendered as one
 * continuous surface rather than a comb of stripes over a flat floor. Every
 * pan carries a faint crown so it reads as gently rolled sheet rather than
 * a flat card, and each groove has its own true bevel normal, so the panel
 * shades like metal under a moving key light rather than a printed pattern:
 * a hard near-white run along each seam's lit lip, a soft AO halo pressed
 * into the groove itself, and the gold holding its saturation across the
 * whole pan rather than crushing to black between ribs.
 *
 * The pointer IS the key light, held in the visitor's hand: x steers the
 * light's yaw, so the highlight riding every seam visibly slides left and
 * right as the cursor moves; y steers pitch, so lifting the pointer flattens
 * the rake across the panel. On top of that the pointer also drops its own
 * warm, near-white bloom directly under the cursor — the sheet catching a
 * light being brought close, not just a rake sliding past. With no pointer
 * present the same light drifts on its own, so a still frame still reads as
 * struck metal rather than a flat gradient.
 *
 * One of the reusable background fields (`ReedField`, `RakeField`,
 * `SlabField`). 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-giltfield__floor`
 * underneath visible — a still corrugated gold frame in the same palette,
 * never a blank box.
 *
 * Like `RakeField` and `ReedField`, the palette is a fixed golden set passed
 * as uniforms rather than read from the token ramp — this is its own
 * material, not the site's accent colour.
 *
 * 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), so copy laid
 * over the field holds WCAG AA with no overlay layer. `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. */
const PALETTE: Record<string, [number, number, number]> = {
  u_dark: [0.05, 0.032, 0.012], // the groove shadow, and the vignette caps
  u_umber: [0.42, 0.22, 0.025], // the shaded flank between gold and groove
  u_gold: [0.89, 0.63, 0.05], // the rib's own lit colour, the panel's body
  u_amber: [1.0, 0.48, 0.04], // the warm cast under the pointer's bloom
  u_hot: [1.0, 0.9, 0.58], // the near-white run along every crest
};

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_umber;
uniform vec3 u_gold;
uniform vec3 u_amber;
uniform vec3 u_hot;

uniform vec4  u_readA;
uniform float u_guard;

/* how many ribs cross the panel's width -- one line to retune density */
const float RIBS = 46.0;

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;     /* aspect-corrected, centred, y up */
  float aspect = res.x / res.y;

  float pres = u_pointer.z;
  vec2  ptrN = (u_pointer.xy - 0.5) * 2.0;                /* -1..1 */
  vec2  ptrP = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);

  /* the panel: mostly flat gold pans, each seamed by one narrow pressed
     groove -- a box-rib profile, not a full sine wave, so the sheet reads
     as broad saturated colour with a fine seam running through it, the way
     the reference panel actually sits, rather than an even black/gold
     pinstripe */
  float cyc = p.x * RIBS;
  float w   = fract(cyc) - 0.5;             /* -0.5..0.5, 0 at the groove */

  float sigma = 0.05;
  float g  = exp(-(w * w) / (2.0 * sigma * sigma));       /* tight: the groove itself */
  float ao = exp(-(w * w) / (2.0 * (sigma * 2.4) * (sigma * 2.4))); /* wider: its soft halo */

  /* the groove's own bevel, plus a faint crown across the rest of the pan
     so it reads as gently rolled metal rather than a perfectly flat card */
  float nx = clamp(-w / sigma * g * 0.95, -1.0, 1.0);
  nx += sin(w * 3.14159265) * 0.1;
  float nz = sqrt(max(1.0 - nx * nx, 0.0));
  vec3  n  = vec3(nx, 0.0, nz);

  /* the key light idles on its own, and the pointer takes it over: yaw
     slides the highlight riding each groove's lip sideways with the
     cursor's x, pitch answers its y so lifting the pointer flattens the
     rake across the sheet rather than skimming its face */
  float idleYaw = sin(u_time * 0.06) * 0.35;
  float yaw   = mix(idleYaw, ptrN.x * 0.9, pres);
  float pitch = mix(0.86 + 0.05 * cos(u_time * 0.05), 0.92 - ptrN.y * 0.12, pres);
  vec3 lightDir = normalize(vec3(yaw * 0.5, 0.1, pitch));

  float diff = max(dot(n, lightDir), 0.0);
  vec3  h    = normalize(lightDir + vec3(0.0, 0.0, 1.0));
  float spec = pow(max(dot(n, h), 0.0), 50.0);

  /* the panel's own colour: saturated gold across the flat pans, rolling
     into umber then the groove's own shadow only right at the seam */
  vec3 base = mix(u_gold, u_umber, ao * 0.55);
  base = mix(base, u_dark, g * 0.85);

  vec3 col = base * (0.5 + 0.62 * diff);
  col += u_hot * spec * (0.55 + 0.55 * pres);

  /* the pointer's own bloom -- gold catching a light held close, on top of
     the rake riding the seams, so the cursor's position reads clearly even
     where its yaw/pitch pull on the key light is subtle */
  float near = exp(-dot(p - ptrP, p - ptrP) * 5.5);
  col += u_amber * near * pres * 0.4;
  col += u_hot * pow(near, 3.0) * pres * 0.35;

  /* a warm corner and a soft vignette, so the sheet reads as lit rather
     than a flat fill */
  vec2 corner = p - vec2(aspect * 0.4, 0.42);
  col += u_amber * exp(-dot(corner, corner) * 2.0) * 0.1;
  float vig = smoothstep(0.0, 0.45, uv.y) * smoothstep(0.0, 0.5, 1.0 - uv.y);
  col *= mix(0.88, 1.0, vig);

  /* ---- the reading guard (see TileField for the full rationale) ---- */
  vec2 rd = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
  float m = mix(max(rd.x, rd.y), length(rd), 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 GiltFieldProps {
  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 GiltField({ className, guardSelector = null }: GiltFieldProps) {
  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 ReedField / RakeField: 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: 2_000_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(_55%_45%_at_18%_22%,oklch(0.88_0.17_92_/_0.9)_0%,transparent_65%_),radial-gradient(_45%_40%_at_78%_72%,oklch(0.58_0.15_65_/_0.5)_0%,transparent_68%_),oklch(0.72_0.16_85)] before:content-[''] before:absolute before:inset-0 before:[background:repeating-linear-gradient(_90deg,oklch(0.08_0.02_65_/_0.55)_0%_1.6%,transparent_1.6%_9%_)] after:content-[''] after:absolute after:inset-0 after:[background:linear-gradient(_180deg,oklch(0.1_0.02_65_/_0.4)_0%,transparent_20%,transparent_80%,oklch(0.1_0.02_65_/_0.45)_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>
  );
}

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