IrisBackgroundsEmber Ridge
Ember Ridge
A near-black field finely corduroyed with vertical ridges, one ember glow rising from below the bottom edge.
Ember Ridge
Built to one reference: a ground of fine vertical ridges — device-pixel lines, not a texture — mostly swallowed by the dark upper frame, lit from below by one warm glow that runs deep red-orange at its core, cooling through umber as it climbs. The ridges read strongest wherever the glow lights them and near-vanish in the dark, the way corduroy does under a low light, rather than a fixed-contrast overlay.
The glow leans a few percent toward the cursor, eased, and a small bloom rides the pointer directly; with no pointer it idles on a slow, barely-there drift. Fixed warm palette, not the token ramp — the ember core lives nowhere in the accent ramp, so the CSS floor carries the colour.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { EmberRidgeField } from "./EmberRidgeField";
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">
<EmberRidgeField />
</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 built to one reference: a near-black ground
* corduroyed with wide vertical ridges, and one warm ember glow rising
* from below the bottom edge — deep red-orange at its core, cooling
* through umber before it's swallowed by the dark upper frame. The
* ridges are pitched in device pixels rather than drawn as a texture, so
* they stay crisp at any DPR and read strongest wherever the glow lights
* them, dimmer in the dark upper field — the same thing corduroy under a
* low light does.
*
* The palette is a fixed warm set passed as uniforms rather than read from
* the token ramp (same reasoning as `RakeField` / `AuroraVeil`) — the CSS
* floor below carries the same four stops as static radial gradients plus
* a repeating-linear-gradient for the ridges, so no-WebGL, a lost context,
* a hidden tab or `prefers-reduced-motion` all leave the identical
* composition on screen, never a blank box.
*
* The pointer leans the glow a few percent toward it, eased, and drops a
* small bloom of its own — a light touch, since the reference has no
* pointer of its own to match. With no pointer the glow idles on a slow,
* barely-there drift.
*
* Drop it into any `position: relative`/`isolate` parent — it fills the
* box. Reading guard: when `guardSelector` resolves to an element, the
* field clamps its own luminance under a ceiling inside that region every
* frame (hue untouched), so copy laid over it holds contrast. `null` (the
* default) turns the guard off.
*/
const PALETTE: Record<string, [number, number, number]> = {
u_ground: [0.035, 0.028, 0.024],
u_umber: [0.14, 0.045, 0.02],
u_ember: [0.46, 0.1, 0.045],
u_amber: [0.92, 0.4, 0.12],
u_hot: [1.0, 0.72, 0.4],
u_plum: [0.24, 0.1, 0.11],
};
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_ground; /* the near-black ridged ground */
uniform vec3 u_umber; /* the glow's outer, cooling ring */
uniform vec3 u_ember; /* the glow's mid band */
uniform vec3 u_amber; /* the glow's bright core */
uniform vec3 u_hot; /* the hottest point at the very centre */
uniform vec3 u_plum; /* a second, muted pool sitting upper-right */
uniform float u_ridge; /* ridge contrast, 0..1 */
uniform float u_ridgeW; /* ridge pitch, in device pixels */
uniform float u_speed; /* idle drift speed */
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);
/* the wash is separable rather than one soft circle: it dies out fast
going up from the bottom edge, and is biased toward the right, so the
right side (including well up toward the top-right corner) carries a
warm cast the left side never picks up, staying tightly pooled near
the bottom rather than flooding the whole frame */
float t = u_time * u_speed;
float xBias = 0.12 * sin(t * 0.6) + (u_pointer.x - 0.5) * 0.1 * u_pointer.z;
float vf = exp(-uv.y * uv.y * 19.0);
float hf = mix(0.1, 1.0, pow(clamp(uv.x + xBias, 0.0, 1.0), 1.3));
float wide = vf * hf;
float mid = wide * wide * 1.35;
/* a small hot pocket right where the glow is brightest, near the
bottom-right corner */
vec2 cornerUV = vec2(0.86 + xBias * 0.4, 0.04 + sin(t * 0.5) * 0.01);
vec2 cd = (uv - cornerUV) * vec2(1.0, 1.7);
float core = exp(-dot(cd, cd) * 10.0);
/* a second, much quieter pool sitting upper-right of centre — muted and
dusty rather than bright, so that region reads as a distinct, cooler
colour from the pure black corners instead of more of the same glow */
vec2 plumC = vec2(0.82 + xBias * 0.2, 0.64);
vec2 pd2 = (uv - plumC) * vec2(0.9, 1.3);
float plum = exp(-dot(pd2, pd2) * 3.2);
vec3 col = u_ground;
col = mix(col, u_plum, clamp(plum * 0.55, 0.0, 1.0));
col = mix(col, u_umber, clamp(wide, 0.0, 1.0));
col = mix(col, u_ember, clamp(mid * 0.9, 0.0, 1.0));
col = mix(col, u_amber, clamp(core * 0.8, 0.0, 1.0));
col += u_hot * pow(core, 3.0) * 0.3;
/* a small bloom that rides the pointer directly, on top of the base glow */
vec2 pd = P - (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
col += u_amber * u_pointer.z * exp(-dot(pd, pd) * 6.0) * 0.08;
/* vertical ridges, pitched in device pixels so they stay crisp at any
DPR and read as a clear corduroy texture, strongest wherever the glow
lights them */
float phase = fract(gl_FragCoord.x / u_ridgeW);
float ridge = smoothstep(0.0, 1.0, abs(phase - 0.5) * 2.0);
col *= mix(1.0 - u_ridge, 1.0, ridge);
/* the very top of the frame reads a touch darker, the way a hero sits
under its own nav shadow */
col *= mix(0.88, 1.0, smoothstep(1.0, 0.4, uv.y));
col = max(col, 0.0);
/* ---- the reading guard ---- */
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 guardBand = 1.0 - smoothstep(0.72, 2.1, m);
col = mix(col, holdUnder(col, 0.15), guardBand * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (1.8 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface EmberRidgeFieldProps {
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;
/** Ridge line contrast, 0–1. Defaults to a clear 0.22. */
ridgeContrast?: number;
/** Ridge pitch, in device pixels. Defaults to a wide 14. */
ridgeWidth?: number;
/** Idle drift speed; 0 freezes the glow in place. */
speed?: number;
}
export function EmberRidgeField({
className,
guardSelector = null,
ridgeContrast = 0.22,
ridgeWidth = 14,
speed = 0.05,
}: EmberRidgeFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const values = { ridgeContrast, ridgeWidth, speed };
const valuesRef = useRef(values);
valuesRef.current = values;
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_ridge", "u_ridgeW", "u_speed", "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_ridge) gl.uniform1f(u.u_ridge, valuesRef.current.ridgeContrast);
if (u.u_ridgeW) gl.uniform1f(u.u_ridgeW, valuesRef.current.ridgeWidth);
if (u.u_speed) gl.uniform1f(u.u_speed, valuesRef.current.speed);
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 (u.u_ridge) gl.uniform1f(u.u_ridge, valuesRef.current.ridgeContrast);
if (u.u_ridgeW) gl.uniform1f(u.u_ridgeW, valuesRef.current.ridgeWidth);
if (u.u_speed) gl.uniform1f(u.u_speed, valuesRef.current.speed);
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"),
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(_46%_34%_at_86%_100%,oklch(0.72_0.19_45)_0%,oklch(0.46_0.16_35)_34%,oklch(0.16_0.05_35)_62%,transparent_82%_),radial-gradient(_38%_30%_at_82%_36%,oklch(0.24_0.05_25_/_0.8)_0%,transparent_75%_),linear-gradient(_90deg,transparent_0%,oklch(0.16_0.05_35_/_0.3)_100%_),oklch(0.04_0.006_50)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(90deg,transparent_0_7px,oklch(0_0_0_/_0.55)_7px_14px_)] after:[mix-blend-mode:multiply]" />
<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