Skip to content

IrisBackgroundsEmber Ridge

Ember Ridge

A near-black field finely corduroyed with vertical ridges, one ember glow rising from below the bottom edge.

glow

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.

  • warm
  • ridged
  • pointer-driven
Family
glow
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

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

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