IrisBackgroundsSwitchback
Switchback
Night Road folded into a mountain hairpin — one ribbon of light zigzags up a dark slope under a pale moonlit haze, tiny valley windows twinkling below.
Switchback
Where `night-road` converges on one vanishing point and `contraflow` rides a single gentle S-bend, this ribbon of warm light trail folds back on itself twice climbing a dark mountainside — the amplitude of each hairpin shrinking with height the way a switchback road genuinely reads from an elevated night vantage: wide and close near the camera, small and distant near the ridge. A pale, cool moonlit haze sits over the top of the frame in place of open black sky, and a sparse scatter of tiny warm points twinkles in the valley away from the road — distant windows, not more traffic.
The pointer's x leans the whole zigzag sideways, more so near the camera than near the ridge, the way the near hairpins would swing more than the distant ones; its y throttles the flicker exactly like `night-road`. The same warm headlight bloom rides the cursor.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { SwitchbackField } from "./SwitchbackField";
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">
<SwitchbackField />
</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 variation on the night-road family that trades `RoadField`'s
* single vanishing point for a genuine zigzag: one ribbon of warm light
* trail folding back on itself twice up a dark mountainside, the amplitude
* of each hairpin shrinking with height the way a switchback road actually
* reads from an elevated night vantage — tight and wide near the camera,
* small and distant near the ridge. A pale moonlit haze sits over the top of
* the frame instead of `RoadField`'s open black sky, and a scatter of tiny
* warm points twinkles in the dark valley away from the road — distant
* windows, not more traffic.
*
* The ribbon's centreline is one `roadCenter(y)` term whose amplitude tapers
* with height and whose frequency is tuned for two full hairpin reversals
* across the frame, rather than `ContraflowField`'s one gentle S-bend — the
* sharper, repeated fold is what reads as a mountain switchback instead of a
* motorway curve. Four lanes ride that centreline as offsets in its
* normalised cross-position, same discipline as the rest of the family.
*
* One pointer interaction, carried over as a driving metaphor rather than a
* new gesture:
* - steering — the pointer's x leans the whole zigzag sideways, more so
* near the camera than near the ridge, the way the near hairpins would
* swing more than the distant ones.
* - throttle — the pointer's y speeds the trail flicker the nearer it sits
* to the bottom edge, same contract as `RoadField`.
* The same warm headlight bloom rides the cursor.
*
* One of the reusable background fields, a sibling of `RoadField` and
* `ContraflowField` in the same trail family. 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-switchbackfield__floor` underneath visible (a
* still composed frame in the same palette, never a blank box).
*
* Fixed palette, not the token ramp — same reasoning as `RoadField`: the
* moonlit ridge haze and the valley-window colour live 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_black: [0.008, 0.009, 0.012], // the near-black mountainside, cooler than RoadField's
u_moon: [0.5, 0.58, 0.74], // the pale cool moonlight haze over the ridge
u_gold: [0.95, 0.74, 0.34], // the main trail colour along the ribbon
u_amber: [0.99, 0.52, 0.18], // the hotter lane
u_flare: [1.0, 0.96, 0.88], // the near-white core riding every lane
u_valley: [0.92, 0.78, 0.45], // the tiny distant valley-window lights
};
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_black;
uniform vec3 u_moon;
uniform vec3 u_gold;
uniform vec3 u_amber;
uniform vec3 u_flare;
uniform vec3 u_valley;
/* 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;
/* A -1..1 triangle wave, same period as sin(x) — sharp V corners at each
extreme rather than a rounded crest, which is what makes the centreline
below read as a folded hairpin instead of ContraflowField's one smooth
S-bend. */
float triangleWave(float x) {
return asin(sin(x)) * (2.0 / 3.14159265);
}
/* The ribbon's centreline — amplitude tapers with height (near hairpins
swing wide, distant ones shrink toward the ridge) and the frequency is
tuned for two full reversals across the frame, the fold that reads as a
switchback rather than one gentle curve. 'steer' leans it sideways, more
near the camera than near the ridge. */
float roadCenter(float y, float steer) {
float amp = mix(0.42, 0.09, smoothstep(0.0, 1.0, y));
return 0.5 + amp * triangleWave(y * 10.5 + 0.6) + steer * mix(1.0, 0.35, y);
}
/* Half-width of the ribbon in P-space, narrowing hard with height so the
distant hairpins read as genuinely far away. */
float roadHalf(float y) {
return mix(0.11, 0.026, smoothstep(0.0, 1.0, y));
}
/* One lane within the ribbon. 'n' is the pixel's normalised cross position
(-1..1); 'id' the lane's own centre in that space. 'along' is the travel
coordinate up the ribbon, driving both the flicker and an fbm-based long
swell so brightness reads as travelling light rather than a static glow. */
float lane(float n, float along, float id, float seed, float spd, out float core) {
float sigma = 0.16;
float dx = n - id;
float body = exp(-dx * dx / (sigma * sigma));
core = exp(-dx * dx / (sigma * sigma * 0.1));
float flicker = 0.75 + 0.25 * sin(along * 0.9 - u_time * 1.3 * spd + seed * 5.0);
float m = fbm(vec2(along * 0.4 + seed * 3.0, seed * 1.7));
float bright = smoothstep(0.05, 0.65, m + 0.05);
return body * flicker * 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);
/* steering: the pointer's x leans the centreline sideways, more near the
camera. throttle: the pointer's y speeds the lane flicker the nearer it
sits to the bottom edge — same contract as RoadField. */
float steer = (u_pointer.x - 0.5) * u_pointer.z * 0.22;
float speed = 1.0 + u_pointer.z * (1.0 - u_pointer.y) * 2.0;
float cx = roadCenter(uv.y, steer);
float hw = roadHalf(uv.y);
float n = (P.x - cx) / hw;
float inRoad = 1.0 - smoothstep(0.85, 1.35, abs(n));
float wz = uv.y / max(1.0 - uv.y * 0.6, 0.15);
float along = uv.y * 7.0 + wz * 1.0;
float c0, c1, c2, c3;
float b0 = lane(n, along, -0.55, 1.1, speed, c0);
float b1 = lane(n, along, -0.18, 2.7, speed, c1);
float b2 = lane(n, along, 0.18, 4.1, speed, c2);
float b3 = lane(n, along, 0.55, 5.9, speed, c3);
vec3 col = u_black;
/* a pale moonlit haze settling over the top of the frame in place of
RoadField's open black sky */
float moonGlow = smoothstep(0.5, 1.0, uv.y);
col += u_moon * moonGlow * 0.06;
/* the ribbon itself */
col += (u_gold * b0 * 1.3
+ mix(u_gold, u_amber, 0.4) * b1 * 1.4
+ mix(u_amber, u_gold, 0.4) * b2 * 1.4
+ u_amber * b3 * 1.3) * inRoad;
col += u_flare * (c0 + c1 + c2 + c3) * 0.55 * inRoad;
/* a faint warm glow bleeding off the ribbon into the dark mountainside —
nothing here is pure black, the discipline the rest of the family keeps */
float glow = exp(-pow(abs(n), 1.4) * 1.3);
col += mix(u_gold, u_amber, 0.5) * glow * 0.05;
/* a scatter of tiny valley-window lights — sparse, twinkling, kept away
from the road and to the lower half where the valley floor would sit */
vec2 gridUv = uv * vec2(38.0, 22.0);
vec2 gid = floor(gridUv);
vec2 gf = fract(gridUv) - 0.5;
float h = dotHash(gid);
float isLight = step(0.965, h);
float twinkle = 0.5 + 0.5 * sin(u_time * 2.0 + h * 40.0);
float dotGlow = exp(-dot(gf, gf) * 220.0);
float valleyMask = (1.0 - inRoad) * smoothstep(0.7, 0.0, uv.y);
col += u_valley * isLight * dotGlow * twinkle * valleyMask * 0.9;
/* the touch: a warm headlight bloom with a near-white core, the same
"your touch joined the light" move RoadField makes */
vec2 ptr = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
vec2 toPtr = P - ptr;
float dPtr = dot(toPtr, toPtr);
col += u_amber * u_pointer.z * exp(-dPtr * 9.0) * 0.5;
col += u_flare * u_pointer.z * exp(-dPtr * 40.0) * 0.4;
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 m = mix(max(rd.x, rd.y), length(rd), 0.4);
float band = 1.0 - smoothstep(0.72, 2.1, m);
col = mix(col, holdUnder(col, 0.09), band * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface SwitchbackFieldProps {
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 SwitchbackField({ className, guardSelector = null }: SwitchbackFieldProps) {
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 RoadField / ContraflowField: 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"
>
<div className="absolute inset-0 [background:radial-gradient(_60%_34%_at_50%_4%,oklch(0.5_0.05_250_/_0.55)_0%,transparent_75%_),radial-gradient(_26%_34%_at_34%_82%,oklch(0.93_0.07_80)_0%,transparent_14%_),radial-gradient(_36%_46%_at_34%_82%,oklch(0.78_0.15_60)_0%,transparent_34%_),radial-gradient(_14%_20%_at_63%_42%,oklch(0.86_0.09_70)_0%,transparent_20%_),radial-gradient(_22%_30%_at_63%_42%,oklch(0.66_0.16_50)_0%,transparent_42%_),radial-gradient(_100%_90%_at_45%_60%,oklch(0.14_0.03_230)_0%,transparent_80%_),oklch(0.02_0.008_230)] after:content-[''] after:absolute after:inset-0 after:[background-image:radial-gradient(_oklch(0.85_0.08_75_/_0.55)_6%,transparent_60%_)] after:[background-size:9%_7%] after:[background-position:0_0] after:[-webkit-mask-image:radial-gradient(_70%_60%_at_78%_78%,#000_0_40%,transparent_78%_)] after:[mask-image:radial-gradient(_70%_60%_at_78%_78%,#000_0_40%,transparent_78%_)]" />
<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