IrisBackgroundsFilament Trails
Filament Trails
Thin warm light filaments fanning from a sparse top into a dense crossing weave.
Filament Trails
Long-exposure light filaments drift across a warm near-black ground — near-horizontal and sparse at the top, fanning into a dense crossing weave toward the base, the brightest threads burning to a near-white core. The look of anamorphic light trails caught in a slow shutter.
Threads near the cursor bow toward it, their flow speeds up, and the nearest cores catch a soft warm bloom, as if the light were being dragged through with a finger.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { FilamentField } from "./FilamentField";
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">
<FilamentField />
</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 long-exposure light filaments — thin warm streaks
* drifting across a warm near-black ground, near-horizontal and sparse at the
* top, fanning into a dense crossing weave toward the base, the brightest
* threads burning to a near-white core. The look of anamorphic light trails
* caught in a slow shutter.
*
* The pointer stirs the trails: threads near the cursor bow toward it, their
* flow speeds up, and the nearest cores catch a soft warm bloom — as if the
* light were being dragged through with a finger.
*
* One of the reusable background fields (`TileField`, `SilkField`,
* `AuroraVeil`, `SpineField`, `SlabField`, `RakeField`, `OpalField`). 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-filamentfield__floor` underneath visible (a still
* composed frame in the same palette, never a blank box).
*
* Like `RakeField` / `AuroraVeil` (and unlike `SilkField`), the palette is a
* fixed warm set passed as uniforms rather than read from the token ramp — the
* near-white filament core lives nowhere in the accent ramp, so the CSS floor
* carries the colour rather than a live token read.
*
* 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_ground: [0.023, 0.014, 0.008], // the warm near-black behind everything
u_ember: [0.223, 0.09, 0.028], // the dim, barely-lit filaments
u_amber: [0.95, 0.46, 0.12], // the mid filaments and the pointer bloom
u_flare: [1.0, 0.86, 0.62], // the near-white core riding the hottest threads
};
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;
uniform vec3 u_ember;
uniform vec3 u_amber;
uniform vec3 u_flare;
/* Reading region for the contrast guard: xy = centre (uv, y up),
zw = half-extent. Measured from the live copy block every frame.
u_guard is 0 when the guard is off. */
uniform vec4 u_readA;
uniform float u_guard;
const int LAYERS = 6;
/* One drifting set of near-horizontal light trails. Returns the soft body of
the streak; writes the near-white core into 'core'. */
float trails(vec2 p, float seed, float freq, float speed, float t, out float core) {
float flow = p.x - t * speed;
float lane = p.y * freq + seed * 7.0;
float laneId = floor(lane);
/* the lanes are not ruler-straight — a slow warp bows each one */
float wob = fbm(vec2(flow * 0.6, laneId * 1.3 + seed)) * 0.34;
float dy = (fract(lane) - 0.5) + wob;
float body = exp(-dy * dy * 240.0);
core = exp(-dy * dy * 1500.0);
/* along-trail brightness: long bright runs broken by gaps, per lane */
float m = fbm(vec2(flow * 0.9 + laneId * 4.0, laneId * 2.0 + seed * 3.0));
float bright = smoothstep(0.02, 0.55, m);
/* not every lane is lit */
bright *= step(0.28, dotHash(vec2(laneId, seed * 13.0)));
core *= bright;
return body * bright;
}
void main() {
vec2 res = u_res / u_scale;
vec2 uv = gl_FragCoord.xy / u_scale / res; /* 0..1, y up */
float aspect = res.x / res.y;
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);
float pull = u_pointer.z * exp(-dP * dP * 22.0);
/* the cursor drags the trails toward it and speeds the local flow up */
vec2 Pw = P - toP * pull * 0.35;
float localSpeed = 1.0 + pull * 2.6;
/* crossing grows toward the base: top trails run flat, lower ones fan */
float depth = clamp(1.0 - uv.y, 0.0, 1.0); /* 0 top .. 1 bottom */
float dens = smoothstep(1.02, 0.12, uv.y); /* sparse up top */
vec3 col = mix(u_ground, u_ember, 0.12 + 0.22 * depth);
float hot = 0.0;
for (int i = 0; i < LAYERS; i++) {
float fi = float(i);
float k = fi / float(LAYERS - 1); /* 0..1 across layers */
float seed = fi * 1.618;
float freq = mix(7.0, 27.0, k);
float speed = mix(0.045, 0.16, fract(seed * 2.0)) * localSpeed;
/* per-layer shear, opened up lower in the frame. Driven by uv.x (0..1,
independent of the box's aspect ratio) rather than the aspect-scaled
P.x a rotation matrix would use — a rotation's vertical displacement
grows with how far a pixel sits from centre, so on a short wide card
(large aspect, P.x reaching much further from 0) the same angle flung
trails through several lanes across the width, reading as stretched
diagonal smears instead of a gentle fan. Shearing by uv.x keeps the
maximum displacement the same shape at any aspect. */
float shearAmt = (fract(seed * 3.0) - 0.5) * (0.55 + depth * 1.7);
vec2 rp = Pw;
rp.y += (uv.x - 0.5) * shearAmt;
float core;
float s = trails(rp, seed, freq, speed, u_time, core);
/* upper layers weigh toward the top, lower layers toward the bottom */
float band = mix(1.0 - depth, depth, k);
float w = mix(0.3, 1.15, band) * dens;
vec3 tint = mix(u_ember, u_amber, smoothstep(0.0, 0.62, s));
col += tint * s * w * 1.7;
col += u_amber * s * s * w * 0.7; /* a little bloom off the body */
hot += core * w;
}
col += u_flare * hot * 1.1;
/* the touch: a warm radial bloom, and the nearest cores flare harder */
col += u_amber * pull * 0.5;
col += u_flare * pull * hot * 0.7;
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.09), guardBand * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface FilamentFieldProps {
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 FilamentField({ className, guardSelector = null }: FilamentFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const guardOn = guardSelector != null;
/* The block the reading guard protects. Looked up once, lazily. */
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 SilkField / RakeField: 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"
data-print="hide"
>
<div className="absolute inset-0 [background:radial-gradient(_120%_80%_at_60%_108%,oklch(0.7_0.19_55)_0%,transparent_52%_),radial-gradient(_90%_60%_at_20%_96%,oklch(0.6_0.14_48)_0%,transparent_50%_),radial-gradient(_120%_120%_at_50%_0%,oklch(0.06_0.02_40)_0%,transparent_60%_),oklch(0.055_0.018_42)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(-4deg,transparent_0_5px,oklch(0.86_0.16_62_/_0.5)_5px_6px),repeating-linear-gradient(6deg,transparent_0_9px,oklch(0.78_0.15_50_/_0.32)_9px_10px)] after:[mask-image:linear-gradient(180deg,transparent_0_22%,oklch(0_0_0_/_0.35)_55%,#000_100%)] after:[-webkit-mask-image:linear-gradient(180deg,transparent_0_22%,oklch(0_0_0_/_0.35)_55%,#000_100%)]" />
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full block opacity-0 transition-opacity duration-[--duration-slow] ease-[--ease-standard] motion-reduce:transition-none 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