Skip to content

IrisBackgroundsWoven Spine

Woven Spine

One thread of warm light down the centre of the frame — a line passing through, not an ornament.

thread

Woven Spine

A single vertical thread on the centre axis: a hot near-white core inside an amber halo, drawn only from the site's one accent ramp. It enters at the top edge, dims through the centre band without breaking, and leaves at the bottom still lit.

The light has grain — drifting vertical fibres, never reseeded, so it reads as static rather than texture — and a slow breath, and it leans toward the pointer where the cursor's height crosses it.

  • warm
  • centred
  • token-palette
Family
thread
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

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

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">
      <SpineField />
    </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";

/**
 * The woven spine as a full-bleed field: one thread of warm light down the
 * centre of the frame — a line passing through rather than an ornament held
 * still. It enters at the top edge, dims through the centre band without
 * breaking, and leaves at the bottom still lit.
 *
 * A hot near-white core inside an amber halo, drawn only from the one accent
 * ramp — canvas at the edges, accent-300 at the line, text-primary at the
 * core. The light has grain (drifting vertical fibres, never reseeded — a
 * per-frame hash reads as static, not texture) and a slow breath, and it
 * leans toward the pointer where the cursor's height crosses it. Nothing
 * here reads scroll position.
 *
 * One of the reusable background fields (`TileField`, `SilkField`,
 * `AuroraVeil`, `SpineField`); the portfolio hero (`hero-story/HeroStory`)
 * is a second caller. Drop it into any `position: relative`/`isolate`
 * parent — it fills the box. Built on `lib/shader-surface.ts`, so
 * degradation is already handled: no WebGL, a blocked or lost context, a
 * hidden tab, or `prefers-reduced-motion` all leave the CSS
 * `.spine-field__floor` gradient stack underneath visible — the same line,
 * still.
 *
 * Reading guard: same mechanism as the sibling fields — when `guardSelector`
 * resolves to an element, the field measures it every frame and clamps its
 * own luminance under a ceiling in that region (hue/saturation untouched).
 * Off by default (`null`).
 */

const FRAG = `
uniform vec2  u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3  u_pointer;

uniform vec3 u_bg;     /* canvas — the ground the frame fades to at its edges */
uniform vec3 u_deep;   /* recessed — the cool floor under the amber */
uniform vec3 u_dim;    /* accent-950 — the outermost breath of the halo */
uniform vec3 u_mid;    /* accent-800 — the halo */
uniform vec3 u_hot;    /* accent-600 — the air right beside the line */
uniform vec3 u_core;   /* accent-400 — the amber sheath around the core */
uniform vec3 u_line;   /* accent-300 — the thread */
uniform vec3 u_bloom;  /* text-primary — the hot core, and the top-edge wash */

/* 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;

/* Widens the thread and its halo without touching the vertical shape or the
   grain: every horizontal falloff is divided by this. 1 is the hero tuning;
   a smaller crop (the bento anchor cell) passes a larger value so the light
   still reads at that size. */
uniform float u_spread;

/* Fine vertical fibres — the light has grain, it is not a clean gradient.
   Two fbm layers stretched almost flat along y; time drifts them, nothing
   reseeds, so they read as strands rather than static. */
float fibres(vec2 uv, float t) {
  vec2 p1 = vec2(uv.x * 150.0,        uv.y * 2.4 + t * 0.02);
  vec2 p2 = vec2(uv.x * 320.0 + 30.0, uv.y * 3.8 - t * 0.015);
  return fbm(p1) * 0.7 + fbm(p2) * 0.3;
}

void main() {
  vec2 res = u_res / u_scale;
  vec2 uv = gl_FragCoord.xy / u_scale / res;   /* x 0..1, y 0 bottom .. 1 top */

  /* The pointer leans the thread toward itself — most where the cursor's
     own height crosses the line, easing out with vertical distance. The
     line reacts to where it is touched; nothing is glued to the cursor. */
  float band = exp(-abs(uv.y - u_pointer.y) * 3.2) * u_pointer.z;
  float sway = (u_pointer.x - 0.5) * band * 0.06;

  float cx = uv.x - 0.5 - sway;
  float ax = abs(cx) / max(u_spread, 0.05);

  /* The field. Two things at once: a wide, soft amber column filling most
     of the frame — dim, never competing with the line, but enough that the
     frame reads as lit rather than empty — and a tighter bright hug close
     to the centre. Neither falls all the way to canvas black; a cool warm
     floor holds the edges. */
  vec3 field = mix(u_bg, u_deep, 0.42);
  field = mix(field, u_dim, exp(-ax * 4.2));
  field = mix(field, u_mid, exp(-ax * 9.5) * 0.92);
  field = mix(field, u_hot, exp(-ax * 22.0) * 0.8);
  field = mix(field, u_core, exp(-ax * 60.0) * 0.7);

  /* Vertical shape: fuller toward the top where the thread enters, lower
     but never dark through the bottom so the eye follows the line down to
     the actions and the section below can pick it up. */
  float airY = 0.44 + 0.56 * smoothstep(-0.15, 0.8, uv.y);
  vec3 col = mix(mix(u_bg, u_deep, 0.32), field, airY);

  float grain = fibres(vec2(cx + 0.5, uv.y), u_time);
  col *= 1.0 + grain * 0.08;

  /* The thread. One line, full height: a hot near-white core inside an
     amber halo.

       notch   a soft dip where the centred headline sits, so the line
               reads behind the type — dimmed, never broken.
       edges   a lift at the very top and very bottom of the frame, so the
               line arrives from above and leaves below rather than
               starting and ending in view.
       along   a gentle overall gain from bottom to top. */
  float dn    = (uv.y - 0.52) / 0.14;
  float notch = 1.0 - 0.62 * exp(-dn * dn);
  float edges = smoothstep(0.90, 1.0, uv.y) * 0.28
              + smoothstep(0.14, 0.0, uv.y) * 0.34;
  float along = 0.8 + 0.2 * smoothstep(0.0, 0.6, uv.y);
  float vis   = clamp(notch * along + edges, 0.0, 1.1);

  float breathe = 0.92 + 0.08 * sin(u_time * 0.3);
  float halo = exp(-ax * 170.0);
  float core = exp(-ax * 760.0);

  col = mix(col, u_line,  halo * 0.8 * vis * breathe);
  col = mix(col, u_bloom, core * 0.9 * vis * breathe);

  /* The pointer brightens the crossing point. */
  col += u_line  * halo * band * 0.5;
  col += u_bloom * core * band * 0.4;

  /* A pale wash across the top of the frame, well above the headline. */
  col = mix(col, u_bloom, smoothstep(0.74, 1.06, uv.y) * 0.13);

  /* ---- the reading guard (see TileField for the full rationale) ---- */
  vec2 rd = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
  float gm = mix(max(rd.x, rd.y), length(rd), 0.4);
  float gband = 1.0 - smoothstep(0.72, 2.1, gm);
  col = mix(col, holdUnder(col, 0.09), gband * u_guard);

  col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.0 / 255.0);

  gl_FragColor = vec4(col, 1.0);
}
`;

export interface SpineFieldProps {
  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;
  /**
   * Widens the thread and its halo. `1` (the default) is the full-viewport
   * hero tuning; pass a larger value in a smaller frame — the bento anchor
   * cell uses ~1.8 — so the light still reads at that size.
   */
  spread?: number;
}

export function SpineField({
  className,
  guardSelector = null,
  spread = 1,
}: SpineFieldProps) {
  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,
      colors: {
        u_bg: "--color-canvas",
        u_deep: "--color-recessed",
        u_dim: "--color-accent-950",
        u_mid: "--color-accent-800",
        u_hot: "--color-accent-600",
        u_core: "--color-accent-400",
        u_line: "--color-accent-300",
        u_bloom: "--color-text-primary",
      },
      uniforms: ["u_readA", "u_guard", "u_spread"],
      onInit: (gl, u) => {
        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);
        if (u.u_spread) gl.uniform1f(u.u_spread, Math.max(spread, 0.05));
      },
      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"),
      onLost: () => canvas.removeAttribute("data-shader"),
      maxPixels: 2_200_000,
      dprCap: 1.5,
    });
  }, [guardSelector, spread]);

  return (
    <div
      className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
      aria-hidden="true"
    >
      <div className="absolute inset-0 [background:linear-gradient(_to_right,transparent_calc(50%_-_0.5px),color-mix(in_oklab,var(--color-accent-300)_86%,var(--color-text-primary))_50%,transparent_calc(50%_+_0.5px)_),linear-gradient(_to_right,transparent_42%,color-mix(in_oklab,var(--color-accent-600)_22%,transparent)_50%,transparent_58%_),radial-gradient(_50%_72%_at_50%_32%,color-mix(in_oklab,var(--color-accent-900)_26%,transparent)_0%,color-mix(in_oklab,var(--color-accent-950)_16%,transparent)_48%,transparent_80%_),linear-gradient(_to_top,var(--color-canvas)_0%,color-mix(in_oklab,var(--color-canvas)_94%,var(--color-recessed)_6%)_28%,color-mix(in_oklab,var(--color-canvas)_85%,var(--color-recessed)_15%)_68%,color-mix(in_oklab,var(--color-canvas)_90%,var(--color-recessed)_10%)_100%_),var(--color-canvas)]" />
      <canvas
        ref={canvasRef}
        className="absolute inset-0 block 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