Skip to content

IrisBackgroundsFlare

Flare

Motorway's own colour rule, lifted out of the road — nine vertical bars running cool white-gold at the centre to full amber at the edges, each breathing on its own; the pointer wakes the bars nearest it.

prism

Flare

Built from `motorway`'s colour rule rather than its road: nine vertical bars, symmetric about the centre, coloured by the exact mapping the highway runs across its six lanes — cool white-gold at the middle, warming outward through gold to full amber at the outer pair. Lifted out of the perspective it was built for, that rule reads as a warm, orderly spectrum in its own right — no vanishing point, no lane dividers, no dashes. Each bar breathes instead: a soft knot of brightness drifts slowly up its own length, and every so often one flares to a near-white pulse, timed so they never do it in unison.

The pointer's touch is a horizontal reach rather than steering or throttle: the bars nearest the cursor's x brighten and swell, the way pressing a finger to a bank of light columns would wake the ones under it, while the rest keep breathing on their own. The same warm bloom with a near-white core still rides the cursor itself.

  • warm
  • pointer-driven
  • bars
Family
prism
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

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

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">
      <FlareField />
    </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 `MotorwayField`'s colour rule, not its road:
 * nine vertical bars, symmetric about the centre, coloured by the exact same
 * mapping the highway uses on its six lanes — cool white-gold at the middle,
 * warming outward through gold to full amber at the outer pair. Lifted out
 * of the perspective it was built for, that rule reads as a warm, orderly
 * spectrum in its own right: no vanishing point, no lane dividers, no
 * dashes, nothing racing toward the camera. Each bar instead breathes — a
 * soft knot of brightness drifts slowly up its own length, and every so
 * often one flares to a near-white pulse — so the field stays alive without
 * ever becoming a road with the cars removed.
 *
 * The pointer's touch is a horizontal reach rather than steering or
 * throttle, the two gestures the rest of the family shares: the bars
 * nearest the cursor's x brighten and swell, the way pressing a finger to a
 * bank of light columns would wake the ones under it, while the rest keep
 * breathing on their own. The same warm bloom with a near-white core still
 * rides the cursor itself — the one touch every field in the catalogue
 * shares.
 *
 * One of the reusable background fields, built from `MotorwayField`'s
 * palette but not a member of the night-road trail family — no perspective
 * projection, no `invZ`, no `wz`. 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-flarefield__floor` underneath visible (a still
 * composed frame in the same palette, never a blank box).
 *
 * Fixed palette, not the token ramp — same reasoning as `MotorwayField`,
 * whose exact three uniforms this reuses: the cool centre and the amber
 * edges 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 — the same three colour stops
   MotorwayField maps across its six lanes, reused here across nine bars. */
const PALETTE: Record<string, [number, number, number]> = {
  u_black: [0.013, 0.013, 0.015], // the near-black ground between the bars
  u_white: [0.92, 0.88, 0.78], // the cool white-gold of the centre bars
  u_gold: [0.98, 0.72, 0.32], // the mid-bar colour
  u_amber: [0.99, 0.46, 0.14], // the outer-bar colour
  u_flare: [1.0, 0.97, 0.9], // the near-white pulse every bar occasionally flares to
};

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_white;
uniform vec3 u_gold;
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;

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);

  float spanHalf = aspect * 0.46;
  float ptrX = (u_pointer.x - 0.5) * aspect;

  vec3 col = u_black;

  for (int i = 0; i < 9; i++) {
    float t = (float(i) - 4.0) / 4.0;                    /* -1..1, centre at i=4 */
    float barX = t * spanHalf;
    float edgeT = abs(t);                                /* 0 centre .. 1 edge */

    /* the exact colour rule MotorwayField runs across its six lanes,
       reapplied here across nine bars: cool centre warming to amber edges */
    vec3 barColor = mix(u_white, mix(u_gold, u_amber, smoothstep(0.35, 1.0, edgeT)), smoothstep(0.0, 0.5, edgeT));

    float sigma = 0.05 + 0.008 * sin(float(i) * 1.7 + 0.4);
    float dx = P.x - barX;
    float body = exp(-dx * dx / (sigma * sigma));

    /* a soft knot of brightness drifting slowly up each bar's own length —
       the movement that stands in for the road's travel, without a road */
    float seed = float(i) * 3.7 + 1.0;
    float travel = uv.y * 2.6 - u_time * (0.16 + 0.05 * fract(seed * 0.61));
    float knot = 0.5 + 0.5 * sin(travel * 6.28318 + seed);
    float glow = mix(0.4, 1.0, smoothstep(0.15, 0.9, knot));

    /* the pointer's reach: a horizontal-only wake that brightens and swells
       the bars nearest the cursor's x, the one gesture unique to this field */
    float near = u_pointer.z * exp(-abs(barX - ptrX) * 2.0);
    glow *= 1.0 + near * 0.9;
    body *= 1.0 + near * 0.35;

    /* an occasional near-white pulse, timed per bar so they never flare in
       unison */
    float pulseM = fbm(vec2(u_time * 0.14 + seed, seed * 1.3));
    float pulse = smoothstep(0.8, 0.97, pulseM);

    col += barColor * body * glow * 1.1;
    col += u_flare * body * pulse * 0.75;
  }

  /* the touch: a warm bloom with a near-white core, the one gesture every
     field in the catalogue shares */
  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.35;
  col += u_flare * u_pointer.z * exp(-dPtr * 40.0) * 0.3;

  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 FlareFieldProps {
  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 FlareField({ className, guardSelector = null }: FlareFieldProps) {
  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 the rest of the catalogue: 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:linear-gradient(_90deg,oklch(0.62_0.18_45)_0%,oklch(0.8_0.14_65)_20%,oklch(0.92_0.05_85)_42%,oklch(0.95_0.02_95)_50%,oklch(0.92_0.05_85)_58%,oklch(0.8_0.14_65)_80%,oklch(0.62_0.18_45)_100%_),oklch(0.03_0.008_60)] [background-blend-mode:normal] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(_90deg,oklch(0.03_0.008_60_/_0.94)_0_6.5%,transparent_6.5%_11.1%_)]" />
      <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