Skip to content

IrisBackgroundsTunnel

Tunnel

Night Road enclosed — three amber-to-crimson lanes race toward a cold glow sitting still at the vanishing point, sodium ceiling lights sweeping overhead in rhythm.

trail

Tunnel

A variation on `night-road` that swaps its open black sky for an enclosure: three amber-to-crimson lane trails converge on the same kind of real perspective projection, but the vanishing point itself is a small, cold white-blue glow — the light at the end of the tunnel — rather than fading to nothing. It is the one thing in frame that sits still while everything else races toward it. Overhead, a run of sodium ceiling lights repeats in world depth exactly the way the rest of the family spaces its dashed dividers, each one growing from a thin distant band to a broad one sweeping past as it nears the camera.

The pointer keeps `night-road`'s contract exactly: x steers the vanishing point — and the exit glow with it — y throttles the lane flicker and the ceiling lights' sweep, and the cursor drops the same warm headlight bloom.

  • warm
  • perspective
  • light-trails
Family
trail
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

example.tsx
import { TunnelField } from "./TunnelField";

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">
      <TunnelField />
    </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 `RoadField`'s night-road family that swaps open
 * black sky for an enclosure: three amber-to-crimson lane trails converge on
 * the same kind of real perspective projection, but the vanishing point
 * itself is a small, cold white-blue glow — the literal light at the end of
 * the tunnel — rather than fading to nothing. Overhead, a run of sodium
 * ceiling lights repeats in world depth exactly the way the dashed divider
 * does in the rest of the family, so each one grows from a thin distant band
 * to a broad one sweeping past overhead as it nears the camera.
 *
 * The exit glow is a fixed screen-space point (`vpX`, `HORIZON`), not gated
 * by the trails' own above-horizon falloff — it is the one thing in frame
 * that isn't racing toward the camera, so it has to read as sitting still
 * while everything else converges on it.
 *
 * Pointer contract carried over from `RoadField` unchanged — steering and
 * throttle, not a new gesture:
 *  - steering — the pointer's x pans the vanishing point (and the exit glow
 *    with it).
 *  - throttle — the pointer's y speeds the lane flicker and the ceiling
 *    lights' sweep the nearer it sits to the bottom edge.
 * The same warm headlight bloom rides the cursor.
 *
 * One of the reusable background fields, a sibling of `RoadField`,
 * `RainlineField`, `SwitchbackField` and `MotorwayField` 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-tunnelfield__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
 * cold exit glow and the sodium ceiling tone 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.014, 0.014, 0.017], // the enclosed tunnel dark, neutral rather than warm
  u_wall: [0.09, 0.07, 0.06], // the faint concrete-wall tone filling the bore
  u_amber: [0.97, 0.5, 0.16], // the main lane colour
  u_crimson: [0.82, 0.14, 0.08], // the secondary, cooler-red lane colour
  u_flare: [1.0, 0.94, 0.84], // the near-white core riding every lane
  u_ceiling: [0.95, 0.62, 0.24], // the sodium ceiling lights sweeping overhead
  u_exit: [0.85, 0.93, 1.0], // the cold light at the end of the tunnel
};

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_wall;
uniform vec3 u_amber;
uniform vec3 u_crimson;
uniform vec3 u_flare;
uniform vec3 u_ceiling;
uniform vec3 u_exit;

/* 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 float HORIZON = 0.74;  /* uv.y of the vanishing point / tunnel exit */
const float VP_X    = 0.0;   /* the point's x offset from centre, P-space */
const float ROAD_W  = 0.5;   /* lane-bundle half-width at the camera, P-space */
const float MIN_BW  = 0.004; /* width floor so the bundle never divides by zero */

float trail(float dx, float sigma, float wz, float seed, float spd, out float core) {
  float s2 = sigma * sigma + 1e-7;
  float body = exp(-dx * dx / s2);
  core = exp(-dx * dx / (s2 * 0.09));
  float flicker = 0.78 + 0.22 * sin(wz * 0.5 - u_time * spd + seed * 6.2);
  return body * flicker;
}

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 pans the vanishing point, the exit glow riding
     along with it. throttle: the pointer's y speeds the lane flicker and the
     ceiling lights' sweep — same contract as RoadField. */
  float steer = (u_pointer.x - 0.5) * u_pointer.z * 0.45;
  float speed = 1.0 + u_pointer.z * (1.0 - u_pointer.y) * 2.2;
  float vpX = VP_X + steer;

  float d = uv.y / HORIZON;
  float invZ = pow(clamp(1.0 - d, 0.0, 1.0), 1.3);
  float aboveHorizon = 1.0 - smoothstep(0.88, 1.02, d);
  float wz = (1.0 - invZ) / max(invZ, 0.02);

  float roadCenterX = vpX * (1.0 - invZ);
  float bw = max(ROAD_W * invZ, MIN_BW);
  float boreHalf = max(ROAD_W * 1.9 * invZ, MIN_BW);
  float inBore = 1.0 - smoothstep(0.9, 1.3, abs(P.x - roadCenterX) / boreHalf);

  vec3 col = u_black;

  /* the tunnel bore itself — a faint wall tone filling the enclosure, so
     nothing here is pure black outside the lit lanes */
  col += u_wall * inBore * (0.4 + 0.3 * (1.0 - invZ));

  /* three lanes, warm amber-to-crimson, converging on the vanishing point */
  float lane1 = roadCenterX - bw * 0.5;
  float lane2 = roadCenterX;
  float lane3 = roadCenterX + bw * 0.5;

  float c1, c2, c3;
  float e1 = trail(P.x - lane1, bw * 0.1, wz, 1.4, 0.85 * speed, c1);
  float e2 = trail(P.x - lane2, bw * 0.09, wz, 3.6, 1.0 * speed, c2);
  float e3 = trail(P.x - lane3, bw * 0.105, wz, 5.2, 0.9 * speed, c3);

  col += mix(u_crimson, u_amber, 0.4) * e1 * 1.5 * aboveHorizon;
  col += u_amber * e2 * 1.6 * aboveHorizon;
  col += mix(u_amber, u_crimson, 0.35) * e3 * 1.5 * aboveHorizon;
  col += u_flare * (c1 + c2 + c3) * 0.6 * aboveHorizon;

  /* the sodium ceiling lights — a band repeating in world depth exactly the
     way the rest of the family spaces its dashed dividers, spanning the
     whole bore rather than one lane, so each one reads as passing overhead
     rather than riding a lane */
  float bandPhase = fract(wz * 0.32 - u_time * 0.85 * speed);
  float bandOn = smoothstep(0.0, 0.05, bandPhase) - smoothstep(0.42, 0.56, bandPhase);
  col += u_ceiling * bandOn * inBore * (0.3 + 0.35 * (1.0 - invZ)) * aboveHorizon;

  /* the light at the end of the tunnel — a fixed, cold glow sitting still at
     the vanishing point while everything else races toward it */
  vec2 exitP = vec2(vpX, HORIZON - 0.5);
  vec2 toExit = P - exitP;
  float dExit = dot(toExit, toExit);
  col += u_exit * exp(-dExit * 260.0) * 1.1;
  col += u_exit * exp(-dExit * 30.0) * 0.35;

  /* 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 TunnelFieldProps {
  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 TunnelField({ className, guardSelector = null }: TunnelFieldProps) {
  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 / RainlineField: 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(_9%_11%_at_50%_27%,oklch(0.97_0.02_235)_0%,transparent_70%_),radial-gradient(_18%_16%_at_50%_27%,oklch(0.82_0.05_215_/_0.5)_0%,transparent_78%_),radial-gradient(_38%_48%_at_50%_70%,oklch(0.86_0.09_55)_0%,transparent_14%_),radial-gradient(_52%_66%_at_50%_70%,oklch(0.62_0.17_35)_0%,transparent_36%_),radial-gradient(_78%_90%_at_50%_70%,oklch(0.3_0.09_30)_0%,transparent_60%_),radial-gradient(_100%_100%_at_50%_60%,oklch(0.1_0.01_30)_0%,transparent_80%_),oklch(0.032_0.01_250)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(_0deg,transparent_0_6%,oklch(0.9_0.05_60_/_0.13)_6%_7.5%_)] after:[-webkit-mask-image:radial-gradient(_58%_78%_at_50%_55%,#000_0_30%,transparent_76%_)] after:[mask-image:radial-gradient(_58%_78%_at_50%_55%,#000_0_30%,transparent_76%_)]" />
      <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