IrisBackgroundsBolt
Bolt
One dominant lightning current, white-hot at the core and wrapped in electric blue, forking down through a near-black sky — never a strike that fades, a current that never leaves.
Bolt
Where the rest of the catalogue's lightning treats a bolt as an event — a flash that forks down and is gone — this one stands: a single current runs the full height of the frame, white-hot at its core, wrapped in an electric-blue halo, throwing off half a dozen forks on its way down. The bolt never leaves the sky; what animates instead is its own path, wandering fbm noise re-tracing a slightly different course through the same shape frame to frame, with a finer, faster layer riding on top to read as crackle rather than sway. A wide, mottled blue haze traces the same path at a much larger radius, standing in for the glow real ionised air holds around a live current.
No pointer interaction — the current holds its ground regardless of the cursor. The wander is the field's only motion, not something a visitor drags around.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { BoltField } from "./BoltField";
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">
<BoltField />
</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 lightning bolt, standing rather than striking — the frame's
* one dominant vertical current, white-hot at its core and wrapped in an
* electric-blue halo, forking into half a dozen branches on its way down
* through a near-black sky.
*
* Every other lightning field in this catalogue (`ThunderheadField`) is
* built around the strike as an event: a bolt appears, flickers, and is
* gone. This one is the opposite premise — the bolt never leaves. What
* animates instead is the path itself: the trunk's every wander and each
* branch's own drift are read off fbm noise fields sampled with time folded
* into their second coordinate, so the current continuously re-traces a
* slightly different course through the same sky, the way a held arc of
* plasma never sits still even when the strike itself does not end. A
* finer, faster noise layer rides the trunk and branches on top of that to
* read as crackle rather than smooth sway.
*
* The branch layout — where each fork leaves the trunk, how far it runs,
* which side it takes — comes from a fixed per-branch hash, so the overall
* shape of the bolt stays recognisable frame to frame; only the wander
* inside that shape keeps moving. A soft, mottled blue haze traces the same
* path at a much wider radius, standing in for the glow real ionised air
* holds around a bolt.
*
* No pointer interaction: the trunk stays put regardless of the cursor.
* A bolt whose whole current shifted left or right as the mouse moved read
* as the composition itself being dragged around, not as something alive —
* the wander described above is the field's only motion.
*
* One of the reusable background fields. 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-boltfield__floor`
* underneath visible — a still bolt in the same palette, never a blank box.
*
* Fixed blue/black/white palette, passed as uniforms rather than the accent
* ramp — this mood lives nowhere in the site's own amber ramp, same
* reasoning as `ThunderheadField`.
*
* 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_ground: [0.006, 0.007, 0.012], // near-black night sky
u_haze: [0.07, 0.14, 0.5], // the wide, mottled blue glow around the current
u_halo: [0.32, 0.58, 1.0], // the bolt's electric-blue halo
u_core: [0.96, 0.98, 1.0], // the bolt's white-hot core
};
const FRAG = `
uniform vec2 u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3 u_ground;
uniform vec3 u_haze;
uniform vec3 u_halo;
uniform vec3 u_core;
uniform vec4 u_readA;
uniform float u_guard;
float hash11(float p) {
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
/* A sharp, angular octave: linear interpolation between per-segment random
offsets, so the path breaks at a hard corner on every segment boundary
instead of easing through it. This is what a real bolt's path actually
looks like — a jagged run of straight kinked segments, not a smooth
curve — where the fbm this used to run (built for soft cloud and glass
surfaces elsewhere in the catalogue) reads as a wavy vine instead. */
float zigzag(float p, float seed) {
float i = floor(p);
float f = fract(p);
float a = hash11(i + seed) * 2.0 - 1.0;
float b = hash11(i + 1.0 + seed) * 2.0 - 1.0;
return mix(a, b, f);
}
/* The trunk's horizontal position at height t (0 top .. 1 bottom). Three
zigzag octaves — a slow coarse bend, a medium kink, a fine jitter — each
sampled with flow (time) folded into its own phase, so the corners
themselves slide over time and the path never repeats a prior frame's
shape exactly. That drift is the whole of what "animate" means for a
bolt that is never off screen. */
float trunkX(float t, float originX, float flow) {
float amp = smoothstep(0.0, 0.02, t);
float bend = zigzag(t * 5.0 + flow * 0.4, 4.0);
float kink = zigzag(t * 13.0 - flow * 0.7, 17.0);
float jitter = zigzag(t * 34.0 + flow * 1.3, 41.0);
return originX + (bend * 0.15 + kink * 0.06 + jitter * 0.02) * amp;
}
/* One fixed fork: idx seeds where it leaves the trunk, how far it runs
and which side it takes, so the bolt's overall silhouette holds still
frame to frame while the kinks inside that shape keep sliding. */
float branchGlow(vec2 P, float t, float originX, float flow, float idx) {
float bStart = 0.08 + hash11(idx * 12.9) * 0.64;
float len = 0.14 + hash11(idx * 5.3 + 2.0) * 0.22;
float side = hash11(idx * 7.7 + 4.0) > 0.5 ? 1.0 : -1.0;
float bt = t - bStart;
if (bt < 0.0 || bt > len) return 0.0;
float forkX = trunkX(bStart, originX, flow);
float amp = clamp(bt / (len * 0.16), 0.0, 1.0) * (1.0 - smoothstep(len * 0.75, len, bt));
float reach = 0.55 + hash11(idx + 9.0) * 0.4;
float k1 = zigzag(bt * 9.0 + idx * 3.0 + flow * 0.9, idx * 13.0 + 2.0);
float k2 = zigzag(bt * 24.0 - flow * 1.6, idx * 9.0 + 70.0);
float x = forkX + side * bt * reach + (k1 * 0.045 + k2 * 0.016) * amp;
float d = abs(P.x - x);
float core = exp(-d * d / (0.0026 * 0.0026));
float halo = exp(-d * d / (0.019 * 0.019));
return core * 0.85 + halo * 0.55;
}
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);
float flow = u_time * 0.1;
float originX = 0.0;
float t = clamp(0.5 - P.y, 0.0, 1.0);
float xTrunk = trunkX(t, originX, flow);
float d = abs(P.x - xTrunk);
/* a faster, finer noise layer riding the trunk reads as crackle rather
than the trunk's own slow sway */
float crackle = 0.86 + 0.14 * fbm(vec2(t * 26.0, u_time * 1.6));
float coreW = mix(0.0075, 0.0048, t);
float haloW = mix(0.05, 0.035, t);
float core = exp(-d * d / (coreW * coreW)) * crackle;
float halo = exp(-d * d / (haloW * haloW)) * crackle;
float branches = 0.0;
branches += branchGlow(P, t, originX, flow, 0.0);
branches += branchGlow(P, t, originX, flow, 1.0);
branches += branchGlow(P, t, originX, flow, 2.0);
branches += branchGlow(P, t, originX, flow, 3.0);
branches += branchGlow(P, t, originX, flow, 4.0);
branches += branchGlow(P, t, originX, flow, 5.0);
/* the wide, mottled haze standing in for the glow real ionised air holds
around a live current */
float haze = fbm(P * 2.3 + vec2(flow * 0.25, -flow * 0.18));
float hazeMask = exp(-d * d / (0.3 * 0.3)) * (0.45 + 0.55 * clamp(haze, 0.0, 1.0));
vec3 col = u_ground;
col += u_haze * hazeMask * 0.9;
col += u_halo * (halo + branches * 0.5) * 0.85;
col += u_core * (core + branches * 0.6);
float vig = 1.0 - smoothstep(0.55, 1.15, length(uv - 0.5));
col *= mix(0.7, 1.0, vig);
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.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface BoltFieldProps {
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 BoltField({ className, guardSelector = null }: BoltFieldProps) {
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 IsobarField / 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_000_000,
dprCap: 1.5,
});
}, [guardSelector]);
return (
<div
className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
aria-hidden="true"
>
<div className="absolute inset-0 [background:linear-gradient(_180deg,oklch(0.97_0.02_250_/_0.85)_0%,oklch(0.72_0.15_258_/_0.5)_10%,transparent_30%_),radial-gradient(_4%_100%_at_50%_50%,oklch(0.85_0.1_255_/_0.55)_0%,transparent_60%_),radial-gradient(_38%_55%_at_40%_32%,oklch(0.32_0.16_260_/_0.5)_0%,transparent_70%_),radial-gradient(_42%_60%_at_60%_70%,oklch(0.28_0.15_258_/_0.45)_0%,transparent_72%_),oklch(0.012_0.007_260)] before:content-[''] before:absolute before:inset-0 before:[background:radial-gradient(_50%_50%_at_6%_8%,transparent_0%,oklch(0.01_0.005_260_/_0.9)_100%_),radial-gradient(_50%_50%_at_94%_92%,transparent_0%,oklch(0.01_0.005_260_/_0.9)_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