Skip to content

IrisBackgroundsTile Lattice

Tile Lattice

A lattice of rounded tiles on one diagonal gradient, with a glint wherever four tiles meet.

tile

Tile Lattice

Rounded tiles carrying one diagonal gradient — indigo, through a dark valley in the middle, to green at the top-right and amber at the bottom-right — with a four-pointed glint at every corner where four tiles meet.

The pointer is real: tiles swell and brighten under the cursor and the nearest glints flare. The palette is a fixed blue/green/amber rather than the Iris accent hue — a deliberate departure.

  • multi-hue
  • lattice
Family
tile
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

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

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">
      {/* Defaults to guarding ".iris-hero__inner" (this site's own hero
          layout) — pass your own selector, or null, outside it. */}
      <TileField guardSelector={null} />
    </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 lattice of rounded tiles carrying one diagonal gradient
 * (indigo → a dark valley through the middle → green at the top-right →
 * amber at the bottom-right), with a four-pointed glint wherever four tiles
 * meet. The pointer is real: tiles swell and brighten under the cursor, and
 * the nearest glints flare.
 *
 * Drop it into any `position: relative`/`isolate` parent — it fills the box.
 * The Iris hero is its first caller; it is written to be reusable.
 *
 * 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-shaderfield__floor`
 * underneath visible (a still composed frame, not a blank box). The one
 * frame a reduced-motion visitor gets is drawn by the surface at a fixed
 * time with the pointer withdrawn.
 *
 * 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 (holdUnder — hue and saturation untouched), so
 * text laid over the field holds WCAG AA with no overlay layer.
 * `docs/design/iris-dna.md` calls for exactly this. Pass `null` for
 * purely decorative use where nothing sits on top.
 *
 * The palette is a fixed blue/green/amber, not `--iris-accent` (iris) —
 * a deliberate, owner-requested departure. See `docs/design/iris-dna.md`
 * if that needs reconciling into the DNA.
 */

/* Palette, sRGB 0–1. Passed as uniforms rather than read from tokens
   because these hues live nowhere in the token set — the CSS floor is the
   degradation fallback instead. */
const PALETTE: Record<string, [number, number, number]> = {
  u_navy: [0.031, 0.075, 0.157],
  u_blue: [0.204, 0.447, 0.882],
  u_green: [0.557, 0.788, 0.302],
  u_orange: [0.945, 0.522, 0.157],
  u_glint: [1.0, 0.957, 0.878],
};

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_navy;       /* the dark valley through the centre */
uniform vec3 u_blue;       /* top-left / bottom-left */
uniform vec3 u_green;      /* top-right */
uniform vec3 u_orange;     /* bottom-right */
uniform vec3 u_glint;      /* the warm-white node sparkle */

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

/* Tiles per unit of centred, aspect-corrected space. One line to retune the
   density of the lattice. */
const float GRID = 6.0;

/* 45deg — the lattice is rotated so tiles read as rounded diamonds and the
   seams (and the glint stars) run on the diagonals. */
const float RC = 0.70710678;

/* Signed distance to a rounded box, zero on the edge, negative inside. */
float sdRoundBox(vec2 p, vec2 b, float r) {
  vec2 d = abs(p) - b + r;
  return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
}

float luma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }

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);             /* centred, square units */

  mat2 R  = mat2(RC, -RC, RC, RC);
  mat2 Ri = mat2(RC,  RC, -RC, RC);

  /* ---- the colour field ------------------------------------------------
     Two diagonal axes carry the gradient; a drifting Gaussian well digs the
     dark valley near the centre. */
  float da = uv.x - uv.y;                              /* -1 top-left .. 1 bottom-right */
  float db = uv.x + uv.y;                              /*  0 bottom-left .. 2 top-right  */

  vec3 field = u_blue;
  field = mix(field, u_green,
              smoothstep(0.85, 1.9, db) * (1.0 - 0.4 * smoothstep(0.15, 1.0, da)));
  field = mix(field, u_orange, smoothstep(0.0, 1.05, da));

  vec2 vc = vec2(0.44 + 0.02 * sin(u_time * 0.18),
                 0.55 + 0.02 * cos(u_time * 0.15));
  vec2 dcv = (uv - vc) * vec2(1.28, 1.0);
  float valley = exp(-dot(dcv, dcv) * 8.5);        /* tight, concentrated well */
  field = mix(field, u_navy, valley * 0.97);

  field *= 0.72 + 0.42 * smoothstep(-0.8, 1.15, da) + 0.16 * smoothstep(0.5, 2.0, db);
  /* the extreme corners glow toward white, as in the reference */
  field += u_green  * 0.16 * smoothstep(1.72, 2.0, db);
  field += u_orange * 0.14 * smoothstep(0.55, 1.1, da);
  float fLuma = luma(field);

  /* ---- the tile lattice ---------------------------------------------- */
  vec2 g   = R * P * GRID;
  vec2 cid = floor(g);
  vec2 fp  = fract(g) - 0.5;

  vec2 ptrP   = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
  vec2 tileC  = Ri * ((cid + 0.5) / GRID);
  float hover = u_pointer.z * exp(-dot(tileC - ptrP, tileC - ptrP) * 26.0);

  float fill = 0.94 + 0.04 * hover;                    /* tiles swell under the cursor */
  float d    = sdRoundBox(fp, vec2(0.5 * fill), 0.22 * fill);
  float aa   = 1.3 * GRID / res.y;                     /* ~1.3 device px, in cell units */
  float tile = 1.0 - smoothstep(-aa, aa, d);

  /* a shallow cushion: a broad top-lit gradient across each rounded square,
     a gentle wide dome, and one soft sheen. screen +y maps to
     (fp.y - fp.x) in the rotated cell. */
  float up   = (fp.y - fp.x) * 0.70710678;             /* -0.5 .. 0.5 */
  float dome = smoothstep(0.5, 0.02, dot(fp, fp) * 1.7);
  float lit  = 0.60 + 0.32 * (up + 0.5) + 0.14 * dome;
  /* a small glossy highlight near the top of each tile */
  float spec = smoothstep(0.22, 0.0, length(fp - vec2(-0.12, 0.12)));
  /* a faint bright bead just inside the whole edge */
  float bevel = smoothstep(0.09, 0.0, abs(d + 0.028));

  vec3 tileCol = field * (0.72 + 0.50 * lit);
  tileCol += u_glint * spec  * (0.06 + 0.16 * fLuma);
  tileCol += u_glint * bevel * (0.04 + 0.08 * fLuma);
  tileCol *= 1.0 + vnoise(P * 150.0) * 0.04;           /* faint surface grain */
  tileCol += tileCol * hover * 0.85;

  vec3 subst = u_navy * mix(0.34, 0.10, valley);       /* near-black seams */

  vec3 col = mix(subst, tileCol, tile);

  /* ---- glints where four tiles meet -------------------------------- */
  vec2 cOff = fract(g + 0.5) - 0.5;                    /* offset to nearest node */
  vec2 cId  = floor(g + 0.5);
  float cr  = length(cOff);
  float core = exp(-cr * cr * 95.0);
  float halo = exp(-cr * cr * 15.0);
  float star = pow(max(0.0, 1.0 - abs(cOff.x) * 2.3), 8.0)
             + pow(max(0.0, 1.0 - abs(cOff.y) * 2.3), 8.0);
  float twinkle = 0.45 + 0.55 * sin(u_time * 1.7 + dotHash(cId) * 42.0);
  float gate = (0.20 + 1.0 * fLuma) * (1.0 - 0.75 * valley);

  float nearPtr = u_pointer.z * exp(-dot(P - ptrP, P - ptrP) * 24.0);

  float glint = (core * 1.5 + halo * 0.34 + star * 0.5) * gate * twinkle;
  glint += (core * 2.2 + halo * 0.8 + star * 1.0) * gate * nearPtr * 2.6;

  col += u_glint * glint * (0.5 + 1.0 * fLuma);

  /* a soft travelling light on the tiles directly under the pointer */
  col += u_glint * 0.10 * nearPtr;
  col = mix(col, tileCol * 1.35, tile * nearPtr * 0.45);

  col = max(col, 0.0);

  /* ---- the reading guard --------------------------------------------
     Where the copy sits (a soft box around the measured copy block, plus
     the header strip at the top), pull the field's luminance under a
     ceiling so the text holds WCAG AA. holdUnder() clamps brightness in
     linear light and leaves hue and saturation alone — the field dims
     itself, there is no overlay layer. */
  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);
  band = max(band, smoothstep(0.78, 1.0, uv.y) * 0.9);
  col = mix(col, holdUnder(col, 0.09), band * u_guard);

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

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

export interface TileFieldProps {
  className?: string;
  /**
   * CSS selector, resolved within the nearest `.iris-hero` (falling back
   * to the document), for the block the reading guard should keep readable.
   * `null` turns the guard off — for decorative use where no text sits on
   * the field.
   */
  guardSelector?: string | null;
}

export function TileField({
  className,
  guardSelector = ".iris-hero__inner",
}: TileFieldProps) {
  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
          ? (canvas.closest(".iris-hero") ?? 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"),
      onIdle: () => canvas.removeAttribute("data-shader"),
      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(_130%_105%_at_15%_85%,oklch(0.55_0.19_264)_0%,transparent_55%_),radial-gradient(_120%_100%_at_87%_11%,oklch(0.78_0.17_138)_0%,transparent_52%_),radial-gradient(_120%_115%_at_92%_91%,oklch(0.72_0.17_55)_0%,transparent_55%_),radial-gradient(_72%_62%_at_44%_46%,oklch(0.22_0.06_264)_0%,transparent_72%_),oklch(0.16_0.04_264)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(_45deg,transparent_0_26px,oklch(0.13_0.03_264_/_0.55)_26px_31px_),repeating-linear-gradient(_-45deg,transparent_0_26px,oklch(0.13_0.03_264_/_0.55)_26px_31px_)]" />
      <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