Tessera Loader
A mosaic of top-lit tiles that light up in a travelling pattern - wave, ripple, rain, spiral, and more.
Loading
Every tile shares one keyframe and differs only by animation delay, so a band of light sweeps the grid. The pattern is pure geometry, so the render is identical on server and client. Under reduced motion each tile freezes at a still snapshot of the wave.
Installation
Complete the shared Setup first, then copy the component into
components/ui/tessera-loader.tsx.
'use client';import * as React from 'react';import { cn } from '@/lib/cn';export type TesseraPattern = | 'wave' | 'ripple' | 'rain' | 'snake' | 'spiral' | 'rows' | 'columns' | 'random';export type TesseraShape = 'square' | 'circle';export type TesseraLoaderProps = { rows?: number; columns?: number; pattern?: TesseraPattern; shape?: TesseraShape; cellSize?: number; gap?: number; accent?: string; speed?: number; label?: string; 'aria-label'?: string; className?: string;};function seeded(n: number) { const x = Math.sin(n * 12.9898) * 43758.5453; return x - Math.floor(x);}function bump(phase: number) { return 0.5 + 0.5 * Math.cos(Math.PI * 2 * phase);}function usePhases(rows: number, cols: number, pattern: TesseraPattern) { return React.useMemo(() => { const cx = (cols - 1) / 2; const cy = (rows - 1) / 2; const spiralRank = new Map<number, number>(); if (pattern === 'spiral') { let top = 0; let bottom = rows - 1; let left = 0; let right = cols - 1; let rank = 0; while (top <= bottom && left <= right) { for (let c = left; c <= right; c++) spiralRank.set(top * cols + c, rank++); for (let r = top + 1; r <= bottom; r++) spiralRank.set(r * cols + right, rank++); if (top < bottom) for (let c = right - 1; c >= left; c--) spiralRank.set(bottom * cols + c, rank++); if (left < right) for (let r = bottom - 1; r > top; r--) spiralRank.set(r * cols + left, rank++); top++; bottom--; left++; right--; } } const raw = (r: number, c: number): number => { switch (pattern) { case 'ripple': return Math.hypot(r - cy, c - cx); case 'rows': return r; case 'columns': return c; case 'snake': return r * cols + (r % 2 === 0 ? c : cols - 1 - c); case 'spiral': return spiralRank.get(r * cols + c) ?? 0; default: return r + c; } }; const total = rows * cols; const phases = new Array<number>(total); if (pattern === 'random') { for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) phases[r * cols + c] = seeded(r * cols + c + 1); } else if (pattern === 'rain') { for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) { const p = r / rows + seeded(c + 1); phases[r * cols + c] = p - Math.floor(p); } } else { let min = Infinity; let max = -Infinity; for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) { const v = raw(r, c); if (v < min) min = v; if (v > max) max = v; } const span = max - min || 1; for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) phases[r * cols + c] = (raw(r, c) - min) / span; } return phases; }, [rows, cols, pattern]);}export function TesseraLoader({ rows = 4, columns = 4, pattern = 'wave', shape = 'square', cellSize = 16, gap = 4, accent = '#f0883e', speed = 1.8, label, 'aria-label': ariaLabel = 'Loading', className,}: TesseraLoaderProps) { const phases = usePhases(rows, columns, pattern); const radius = shape === 'circle' ? 9999 : Math.max(4, Math.round(cellSize * 0.3)); const vars = { '--tsr-accent': accent, '--tsr-speed': `${speed}s`, } as React.CSSProperties; return ( <div role="status" aria-label={ariaLabel} aria-busy="true" style={vars} className={cn('tessera inline-flex flex-col items-center gap-3', className)} > <div aria-hidden className="grid" style={{ gap, gridTemplateColumns: `repeat(${columns}, ${cellSize}px)`, }} > {phases.map((phase, i) => ( <div key={i} style={{ width: cellSize, height: cellSize, borderRadius: radius }} className={cn( 'tessera-cell bloom-edge relative', 'bg-gradient-to-b from-white to-[#f4f4f5] dark:from-[#1d1d1d] dark:to-[#141414]', 'shadow-[inset_0_1px_0_0_rgba(255,255,255,0.5)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.06)]', )} > <span className="tessera-lit" style={ { '--tsr-delay': `${(-phase * speed).toFixed(3)}s`, '--tsr-static': (0.08 + 0.92 * bump(phase)).toFixed(3), } as React.CSSProperties } /> </div> ))} </div> {label ? ( <span className="text-[12px] font-medium tracking-[-0.01em] text-neutral-500 dark:text-[#8b8b8b]"> {label} </span> ) : null} </div> );}Add the animation to your global.css.
.tessera {
--tsr-accent: #f0883e;
--tsr-speed: 1.8s;
}
.tessera-lit {
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--tsr-accent) 55%, #ffffff),
var(--tsr-accent)
);
box-shadow:
inset 0 1px 0 0 rgba(255, 255, 255, 0.55),
0 1px 2px 0 rgba(0, 0, 0, 0.35),
0 0 6px -2px var(--tsr-accent);
opacity: 0;
will-change: opacity;
animation: tessera-pulse var(--tsr-speed) linear infinite;
animation-delay: var(--tsr-delay, 0s);
}
@keyframes tessera-pulse {
0%,
100% {
opacity: 0;
}
14% {
opacity: 1;
}
42% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.tessera-lit {
animation: none;
opacity: var(--tsr-static, 0.5);
}
}Usage
import { TesseraLoader } from '@/components/ui/tessera-loader';
export default function Example() {
return <TesseraLoader label="Loading" />;
}Examples
Eight travelling patterns, each with its own accent.
wave
ripple
rain
spiral
snake
random
Any grid size and either tile shape.
circle
3 x 6
5 x 5 spiral
Props
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | 4 | Grid rows. |
columns | number | 4 | Grid columns. |
pattern | TesseraPattern | "wave" | How the light travels the grid. |
shape | "square" | "circle" | "square" | Tile shape. |
cellSize | number | 16 | Tile size in px. |
gap | number | 4 | Gap between tiles in px. |
accent | string | "#f0883e" | Accent the lit tiles glow with. |
speed | number | 1.8 | Seconds for one full cycle. |
label | string | - | Optional caption under the grid. |
className | string | - | Forwarded to the root. |
pattern is one of wave, ripple, rain, snake, spiral, rows,
columns, or random.