liten

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.

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.

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

Example.tsx
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

PropTypeDefaultDescription
rowsnumber4Grid rows.
columnsnumber4Grid columns.
patternTesseraPattern"wave"How the light travels the grid.
shape"square" | "circle""square"Tile shape.
cellSizenumber16Tile size in px.
gapnumber4Gap between tiles in px.
accentstring"#f0883e"Accent the lit tiles glow with.
speednumber1.8Seconds for one full cycle.
labelstring-Optional caption under the grid.
classNamestring-Forwarded to the root.

pattern is one of wave, ripple, rain, snake, spiral, rows, columns, or random.

On this page0%