IrisBackgroundsSkein
Skein
Seven nested light-blue arcs of particle dust sweeping up from the left edge and back down past the right, over a navy ground that lifts to a soft glow at the crest — a direct build of the reference photo.
Skein
Seven arcs, nested loosely rather than concentric, sweep up from below the left edge, crest left-of-centre, and run back down toward the right — each one drawn not as a stroke but as a stream of particle dust: dense and near-solid along its own centreline, thinning into scattered specks the further a point drifts from it. The innermost arcs read as a tight, bright cable; the outermost fray wider and speckle more, the way the reference photo's own outer trails loosen. A soft blue glow sits over the crest, cooling into a darker navy toward the corners.
The dust never sits still: each arc's particle field slides slowly along its own length, alternating direction arc to arc, so the bundle reads as light actually travelling rather than a printed texture. The pointer takes hold two ways — near the crest, the whole bundle leans toward it, the way pulling one end of a loose cable drags the rest; everywhere else, a soft spotlight rides the cursor directly, brightening whichever arcs pass under it.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { SkeinField } from "./SkeinField";
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">
<SkeinField />
</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 from one reference photo: a bundle of seven
* nested light-blue arcs, sweeping up from below the left edge, cresting
* left-of-centre, and running back down toward the right — each arc drawn
* not as a solid stroke but as a stream of particle dust, dense and bright
* along its own centreline and thinning into scattered specks the further
* a point sits from it. Inner arcs read as a tighter, brighter cable;
* outer ones spread wider and speckle more, the way the reference photo's
* outermost trails fray. A soft blue glow sits over the crest, cooling to
* a darker navy toward the corners.
*
* The dust is never still: each arc's particle field slides slowly along
* its own length, alternating direction arc to arc, so the bundle reads as
* light actually travelling rather than a printed texture.
*
* The pointer does two things: near the crest, the whole bundle leans
* toward it, the way pulling on one end of a loose cable bundle drags the
* rest; and everywhere else, a soft spotlight of light rides the cursor
* directly, brightening whatever arcs pass under it. Move away and both
* ease back to rest.
*
* One of the reusable background fields (`NovaField`, `CoronaField`, …).
* 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-skeinfield__floor`
* underneath visible — a still frame in the same palette, never a blank
* box.
*
* Like `NovaField`, the palette is a fixed set passed as uniforms rather
* than read from the accent ramp — the reference photo's saturated blue
* lives nowhere in the site's ramp, so the CSS floor carries the colour
* rather than a 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). `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_deep: [0.035, 0.075, 0.16], // darkest navy, the corners
u_navy: [0.1, 0.19, 0.36], // mid ground behind the bundle
u_glow: [0.42, 0.6, 0.82], // soft ambient wash over the crest
u_particle: [0.62, 0.78, 0.95], // the dust each arc is made of
u_core: [0.93, 0.97, 1.0], // the hot white centreline
};
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_deep;
uniform vec3 u_navy;
uniform vec3 u_glow;
uniform vec3 u_particle;
uniform vec3 u_core;
uniform vec4 u_readA;
uniform float u_guard;
const int STRANDS = 7;
/* One arc's particle-dust plus hot centreline, worked in the arc's own
polar space: theta runs its length, radial distance from the arc's own
circle is the perpendicular offset a dust speck can drift from it. */
float strandGlow(vec2 p, vec2 center, float radius, float widthAcross,
float density, float speed, float seed, out float core) {
vec2 rel = p - center;
float r = length(rel);
float theta = atan(rel.x, rel.y); /* 0 = straight up from centre */
float edgeFade = 1.0 - smoothstep(0.95, 1.55, abs(theta));
float dRadial = r - radius;
core = exp(-dRadial * dRadial / (2.0 * widthAcross * widthAcross * 0.05));
float along = theta * radius - u_time * speed;
float across = dRadial;
float pitchAlong = 0.048;
float pitchAcross = widthAcross * 0.55;
vec2 cellId = floor(vec2(along / pitchAlong, across / pitchAcross));
float h0 = vhash(cellId + seed);
float acrossCenter = (cellId.y + 0.5) * pitchAcross;
float densFall = exp(-acrossCenter * acrossCenter / (2.0 * widthAcross * widthAcross));
float dust = 0.0;
if (h0 < density * densFall) {
vec2 jitter = vec2(vhash(cellId + vec2(1.7, 3.1) + seed),
vhash(cellId + vec2(9.2, 4.6) + seed)) - 0.5;
vec2 particle = (cellId + 0.5 + jitter * 0.9) * vec2(pitchAlong, pitchAcross);
vec2 d = vec2(along, across) - particle;
float sizeR = mix(0.16, 0.62, vhash(cellId + vec2(5.5, 2.2) + seed)) * pitchAcross;
dust = exp(-dot(d, d) / (sizeR * sizeR * 0.5));
}
return (core * 0.9 + dust) * edgeFade;
}
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);
/* the bundle leans toward the pointer when it's near the crest, the way
pulling one end of a loose cable drags the rest of it */
vec2 ptr = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
vec2 apex = vec2(-0.12, 0.14);
vec2 toApex = ptr - apex;
float grip = u_pointer.z * exp(-dot(toApex, toApex) * 1.4);
vec2 bundleShift = toApex * grip * 0.3;
vec2 baseCenter = vec2(-0.12, -1.85) + bundleShift;
vec3 col = mix(u_navy, u_deep, smoothstep(0.15, 1.3, length(p - vec2(0.0, -0.15))));
vec2 glowAt = apex + bundleShift * 0.6;
float dGlow = length((p - glowAt) * vec2(1.0, 1.35));
col += u_glow * exp(-dGlow * dGlow * 1.1) * 0.85;
float hotSum = 0.0;
for (int i = 0; i < STRANDS; i++) {
float fi = float(i);
float t = fi / float(STRANDS - 1); /* 0 inner .. 1 outer */
float radius = mix(1.72, 2.28, t);
vec2 center = baseCenter + vec2(mix(0.0, -0.05, t), 0.0);
float width = mix(0.018, 0.05, t);
float density = mix(0.85, 0.34, t);
float speed = mix(0.05, 0.14, t) * (mod(fi, 2.0) < 0.5 ? 1.0 : -1.0);
float seed = fi * 11.7;
float core;
float g = strandGlow(p, center, radius, width, density, speed, seed, core);
vec3 tint = mix(u_particle, u_core, core);
col += tint * g * mix(1.5, 0.85, t);
hotSum += core * mix(0.5, 0.15, t);
}
/* the pointer's own spotlight, everywhere on the field, not only near
the crest — a soft lift that rides the cursor directly */
float distP = dot(p - ptr, p - ptr);
float spot = exp(-distP * 4.5) * u_pointer.z;
col += u_core * spot * 0.22;
col += u_glow * hotSum * 0.3;
vec2 corner = uv - 0.5;
float cvig = length(corner * vec2(1.05, 1.2));
col *= mix(1.0, 0.62, smoothstep(0.45, 1.0, cvig));
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.1), guardBand * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.0 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface SkeinFieldProps {
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 SkeinField({ className, guardSelector = null }: SkeinFieldProps) {
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 NovaField / CoronaField: 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: 1_800_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(_120%_130%_at_44%_168%,transparent_46%,oklch(0.82_0.07_235_/_0.5)_52%,oklch(0.68_0.13_235_/_0.38)_58%,transparent_64%_),radial-gradient(_45%_55%_at_40%_8%,oklch(0.62_0.09_235_/_0.55)_0%,transparent_68%_),radial-gradient(_60%_70%_at_45%_60%,oklch(0.28_0.08_250)_0%,oklch(0.14_0.05_255)_70%,oklch(0.06_0.03_255)_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>
);
}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