A grid that pays attention

One of the internal tools I built at work is, at its heart, a capability map: things down the side, places across the top, and in each box a number for how often that thing has been done there, with a small amber chip for the ones still under way. It is a table. People have been making tables since clay. So the work was all in the small stuff: making a dense grid of numbers feel like it notices you.

Here it is again with invented rows and columns, wired up with the same CSS and the same JavaScript.

across the grid

Click a row or column name, or pick up to three boxes.

Move over it, press a box, click a row or column name, pick a few boxes, search. Esc clears.

Arriving on a diagonal

When the grid loads, every box gets a custom property --d of (row + column) × 0.035s, so the whole thing washes in from the top-left corner like a tide. Each box does three things on that clock: its bar grows from the left over 0.9s, its number fades up 2px at --d + 0.15s, and the amber chip pops at --d + 0.45s on cubic-bezier(.34, 1.56, .64, 1), a curve that overshoots a little so it lands with a bounce. The chip waits for its bar on purpose; things that arrive in a sensible order read as one gesture instead of noise.

The numbers count up from zero over 650ms with an ease-out cubic, 1 − (1 − t)³, starting just after the box's own delay. With reduced motion, the server's plain number is what you get.

.track i { animation: map-grow .9s var(--ease) both; animation-delay: var(--d); }
.top     { animation: map-fade .6s var(--ease) both; animation-delay: calc(var(--d) + .15s); }
.mchip   { animation: map-pop .5s cubic-bezier(.34, 1.56, .64, 1) both;
           animation-delay: calc(var(--d) + .45s); }

Following the pointer

The wrapper has a ::before with a radial-gradient(420px circle at var(--mx) var(--my)) of the accent at 7%, and a pointer listener that just sets those two properties. It is barely there, which is the point; you feel it more than see it. Hovering a box lights its row name and column header (a crosshair, so you never lose your place in an eight-by-seven of numbers), lifts it 2px, and pressing sends a ripple out from under your finger: a 12px dot scaling to 16 times its size over 0.7s while it fades.

Focusing

When something is chosen, the grid goes into a dim mode: everything else drops to 28% opacity and shrinks to 0.97, and the chosen boxes glow and arrive one after another, each delayed by its place in the list. Hovering the legend isolates a layer, so you can see only the done numbers, only the under-way chips, or only the empty boxes. Search does the same trick live as you type, with a 40ms cascade per match so the hits ripple in rather than blink on.

Wires and links

Click a column name and wires fan out from it to every box it touches: cubic Béziers that leave the label vertically and arrive on top of each box, each measured with getTotalLength() so the dash offset can draw it in, staggered by 40ms. A row name's boxes all sit on its own line, so for this demo I bent the same curves into a fan that arcs over the row. Follow a path chains boxes with S-curves instead and pins a numbered badge on each, and the boxes light in order at 160ms apart rather than 50.

Pick two or three boxes and they get linked, with how much they share on a little pill. The lines start at the edges of the boxes rather than their centres, which sounds trivial and makes all the difference. And if a third picked box sits in the way, the link bows out around it instead of running through it. The real thing lets you pick eight; three keeps this one readable.

// Where the segment from the centre of r towards (dx, dy) leaves the box
function edge(r, dx, dy) {
  const t = Math.min(dx ? r.w / 2 / Math.abs(dx) : Infinity,
                     dy ? r.h / 2 / Math.abs(dy) : Infinity);
  return { x: r.x + dx * t, y: r.y + dy * t };
}

With reduced motion, all of it goes still except the crosshair, because knowing where you are isn't decoration. In the demo I also made the legend tappable, since hover doesn't exist on a phone.

Built
October to November 2026
Stack
Next.js, React, CSS custom properties, SVG
This page
Vanilla JS and CSS, no libraries
Lives at
An internal tool at work
Source
Private

Built with Claude Code. Every row, column and number on this page is invented.