Borough

Seven years on one block.

Fig. 1 — Borough, live captureOpen the live demo ↗10 s loop · recorded frame by frame on GPU

Field notes

A by-the-numbers page that is a small town on a floating block. Each corner is one number; drag the years and the buildings go up inside their scaffolds, the numbers count with them and dusk falls until the windows light. Click a building and it tells you the year it went up.

  • 3D
  • Pointer interaction
  • SaaS
  • API & infra

Borough is the stats page of Kettleyard, a fictional hosting platform, built as an isometric town on a floating block of soil and grass with two roads crossing it. Each corner is one of its four numbers: houses for teams, works with chimneys for deploys a day, towers with blinking lamps for edge cities, shops with awnings for countries. Drag the year slider from 2019 to 2026 and every corner grows on a log scale: a scaffold goes up, the building rises inside it, the scaffold comes down, and the numbers beside the town count with it. The years also bring dusk, so by the last one the sky is navy and every window is lit.

Click any building and a tag says when it went up and what the number had just passed ("Went up in 2023, when edge cities passed 83"); the first ones say they were standing from the start. Hover or focus a number and its corner lifts; drag the block to turn it. Every number and string lives in one file. It is plain three.js with boxes, cones and cylinders, no models or images. Reduced motion opens on the last year, without WebGL the numbers, slider and dusk still work, and on a phone the town sits above the numbers and a vertical swipe still scrolls the page.

How it's built

  • The year is the only input that matters: each corner's progress is how far its number has grown from the first year to the last on a log scale, so a jump from 3 to 9 reads as much growth as 120 to 360. Each lot has a threshold on that progress; its scaffold, its rise and the scaffold coming down are finite smoothsteps around it, and the last lot is placed so that at the final year every building is up and every scaffold is gone.
  • The state keeps only what was asked for and when (the slider's year, when Replay or a focus started, which building was clicked), never how far along anything is. pose(frame) in src/lib/town.ts turns one moment into every number on screen, so the preview is recorded frame by frame and a check compares second 10 with second 0.
  • A tag never guesses: the year it names is found by bisection, the moment that building was half up, and the value is the number at that moment. A Node check proves each tag against the growth curve and that the first buildings say "Standing since 2019" instead of inventing a year.
  • Cars cross the block once per loop and shrink to nothing at its edges, so their lap wraps where no one can see it; the tower lamps blink five times per loop. The tour runs the years up, holds at night with one tower's tag up, then rewinds the slider, and a first touch takes over with the same year, turn and glow.
  • Dusk is a function of the year: the lights dim, the sun warms, the windows come on and the page's own colours follow through CSS variables. The canvas is watched with a ResizeObserver, not the window, because the grid resizes it on its own.

Inside the prompt

Every file below is written out in full, so your agent creates the project exactly — no guessing, no missing pieces.

borough-stats/
├─ .gitignore
├─ check.ts
├─ index.html
├─ package.json
├─ tsconfig.json
├─ vite.config.ts
└─ src/
   ├─ App.tsx
   ├─ index.css
   ├─ main.tsx
   ├─ vite-env.d.ts
   ├─ components/
   │  ├─ Page.tsx
   │  └─ Scene.tsx
   ├─ lib/
   │  ├─ copy.ts
   │  ├─ live.ts
   │  ├─ loop.ts
   │  ├─ time.ts
   │  ├─ timeline.ts
   │  └─ town.ts
   └─ scene/
      └─ build.ts

Recut it

  • Rename the company and change the four numbers, their years and labelssrc/lib/copy.ts + index.html → COMPANY / YEARS / STATS / COPY
  • Change what each corner is built from, and its coloursrc/lib/copy.ts + src/scene/build.ts → Stat.kind / Stat.color / building()
  • Re-colour day and nightsrc/lib/town.ts + src/index.css → NIGHT / --bo-…
  • Retime the scaffolds, the dusk and the trafficsrc/lib/town.ts → GROW / SCAFFOLD_UP / SCAFFOLD_DOWN / duskAt / carAt
  • Retime the 10-second tour and Replaysrc/lib/timeline.ts → tourYear / TOUR_PICK / PLAY
  • Check the loop still closes and every tag tells the truthcheck.ts → pnpm check

Also in the vault