IrisBackgroundsAmber Glow
Amber Glow
A single pool of honey-gold light, blurred past the point of having an edge, off-centre on a pale cream ground.
Amber Glow
Built to one specific reference photo: no bands, no rings, just a three-stop radial fall from cream through amber into a saturated gold core, its boundary pushed gently irregular by one low-frequency warp so it reads as an out-of-focus light source rather than a drawn ellipse. The second light-ground field in the catalogue alongside `opal-wash`, and considerably quieter than it — one warm pool instead of a flowing multi-band wash.
The pool leans a few percent toward the cursor, eased rather than snapped, and a second, smaller halo rides the cursor directly — a soft warm highlight that fades in and out with presence alone. Both settle back to a slow, barely-there idle drift the moment the pointer leaves. Its reading guard washes toward cream for dark copy, the same inverse contract as `opal-wash`.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { AmberGlowField } from "./AmberGlowField";
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">
<AmberGlowField />
</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";
/**
* Built to one specific reference photo: a single pool of warm honey-gold
* light sitting off-centre on a pale cream ground, blurred past the point of
* having an edge — no band, no ring, just a three-stop radial fall from cream
* through amber into a saturated gold core, its boundary pushed gently
* irregular by one low-frequency fbm warp so it reads as an out-of-focus
* light source rather than a drawn ellipse.
*
* Like `opal-wash`, this is a light-ground field: the reading guard washes
* *toward* cream instead of clamping down, because copy laid over it is
* dark, not light.
*
* Two micro-interactions ride the shared pointer from `lib/shader-surface.ts`
* rather than a heavier flow field: the glow itself leans a few percent
* toward the cursor (eased, the same lag every pointer-driven field in this
* catalogue uses, so it never snaps), and a second, smaller highlight rides
* the cursor directly — a soft warm halo that fades in with presence, the
* "a light is listening" cue. Both settle back to a slow, near-still idle
* drift the instant the pointer leaves.
*
* Drop it into any `position: relative`/`isolate` parent — it fills the box.
* Every degradation path `lib/shader-surface.ts` handles is already covered:
* no WebGL, a blocked or lost context, a hidden tab, or
* `prefers-reduced-motion` all leave the CSS `.iris-amberglowfield__floor`
* underneath visible — a still frame in the same three stops, never a blank
* box.
*/
/* Palette, sRGB 0–1. Uniforms, not tokens — this warm-yellow mood lives
nowhere in the site's own amber ramp, same reasoning as `opal-wash`. */
const PALETTE: Record<string, [number, number, number]> = {
u_cream: [0.996, 0.976, 0.906], // pale ivory ground, the corners
u_amber: [0.984, 0.804, 0.298], // mid transitional warmth
u_gold: [0.961, 0.706, 0.039], // the saturated core
};
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_cream;
uniform vec3 u_amber;
uniform vec3 u_gold;
uniform vec4 u_readA;
uniform float u_guard;
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);
/* idle drift — a slow ellipse, amplitude small enough to never read as
motion, same restraint as GradientBackground's default speed */
float t = u_time * 0.045;
vec2 drift = vec2(cos(t * 0.8), sin(t * 0.6)) * 0.02;
/* micro-interaction 1: the pool leans toward the cursor. u_pointer.xy
is already the eased position lib/shader-surface.ts tracks per frame,
so this needs no easing of its own — presence alone fades the pull. */
vec2 ptr = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
vec2 pull = ptr * 0.07 * u_pointer.z;
vec2 blobC = vec2(aspect * 0.30, 0.02) + drift + pull;
/* one low-frequency warp so the pool's edge reads as an out-of-focus
light source, not a drawn ellipse — kept subtle, this is a blur
reference, not a flow field like opal-wash. */
vec2 warp = vec2(fbm(P * 1.0 + t * 0.3), fbm(P * 1.0 - t * 0.24 + 9.0)) * 0.07;
/* falloff is cut with an explicit smoothstep on top of the exponential
tails, not left to decay on its own — a pure gaussian never reaches
true zero, which is what was casting the whole frame amber instead of
leaving the corners genuinely cream, the way the reference photo has
real pale space around the pool rather than a wall-to-wall wash. */
vec2 d = (P + warp - blobC) / vec2(0.46, 0.38);
float r2 = dot(d, d);
float falloff = smoothstep(3.4, 0.0, r2);
float core = exp(-r2 * 3.2) * falloff;
float mid = exp(-r2 * 1.1) * falloff;
vec3 col = u_cream;
col = mix(col, u_amber, clamp(mid, 0.0, 1.0));
col = mix(col, u_gold, clamp(core, 0.0, 1.0));
/* micro-interaction 2: a small warm halo riding the cursor itself,
distinct from the main pool — fades in and out with presence alone, so
it never snaps on or off. */
float dCursor = dot(P - ptr, P - ptr);
float halo = exp(-dCursor * 9.0) * u_pointer.z;
col = mix(col, vec3(1.0, 0.98, 0.9), halo * 0.4);
col += u_gold * halo * 0.22;
col = clamp(col, 0.0, 1.0);
/* ---- the reading guard ---- */
/* Pale ground, dark copy — this washes *toward* cream instead of
clamping down, the inverse of the catalogue's dark fields. */
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, mix(col, u_cream, 0.72), guardBand * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface AmberGlowFieldProps {
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 AmberGlowField({ className, guardSelector = null }: AmberGlowFieldProps) {
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 OpalField/DuskBloomField: 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="iris-amberglowfield__floor absolute inset-0 [background:radial-gradient(_42%_40%_at_68%_54%,oklch(0.83_0.17_85)_0%,oklch(0.9_0.13_88)_24%,oklch(0.97_0.03_95)_46%,oklch(0.985_0.012_95)_70%_)]" />
<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