← Work
Interface and information architectureBuilt and running2026

The AI Museum

Nine years of AI history built as a building you walk through, where one exhibit is made entirely of missing data.

Next.jsTypeScriptTailwind v4React Three FiberThree.jsFramer Motion

The problem

Nine years of AI history is a subject with no natural shape. Told as a list it is a Wikipedia page nobody finishes. Told as a timeline it becomes a horizontal scroll with the same card repeated thirty-one times, and a reader learns the dates without ever feeling the change.

The harder problem is that the interesting part of this history is not the sequence of releases. It is the shift in what the field was willing to tell you about itself, and a format that only lists achievements cannot show an absence.

So it was built as a building rather than a page: eight rooms a reader walks through, where the architecture carries the argument and one exhibit is made entirely of missing data.

What was built

Forty-three pages at /ai-museum. Thirty-one milestone exhibits across five era rooms, six pioneer portraits, five artifact essays, a lobby with a floor plan, and a guided tour of eight works for a reader with fifteen minutes rather than an afternoon.

Every room is a full-bleed section with its own wall colour, running warm to cool as time advances, so the early years read as old paper and the present as a contemporary white cube. Each room is entered through a threshold: a centred panel with the era numeral, name, years and work count, which is a deliberate pause rather than a heading.

Content is data, not markup. Three TypeScript arrays and forty-two JSON files drive all forty-three pages, the sitemap, the structured data, the homepage timeline and the guided tour. Nothing is hand-authored twice, which is what makes the museum extendable at all.

The gallery closes on a chart of every disclosed parameter count on a log scale, from the Transformer's 65M to Kimi K3's 2.8T. Three.js spheres mark the entrance and the exit, and navigating from a work in the gallery to its exhibit page morphs the title across the navigation rather than replacing the page.

How it is put together

Next.js App Router, statically generated. Every exhibit is a route built at compile time from the data files, so there is no database and no server doing work at request time. Tailwind v4 with tokens declared in CSS rather than a config file, Framer Motion for reveals, React Three Fiber for the two spheres.

The load-bearing decision is that the era definitions live in exactly one place. Room colour, accent, year bounds and work count are read by the gallery, the sticky signage, the floor plan and every exhibit page. A milestone whose year falls outside every era range renders nowhere and fails silently, which is the one sharp edge in the model and is documented where someone will hit it.

The path of one document

  1. 01A milestone is one entry in a TypeScript array plus one JSON file matching a typed shape.
  2. 02Its year, a decimal, places it in exactly one era, which determines the room it hangs in and the accent colour it carries.
  3. 03The gallery renders one section per era, painted with that era's wall tint, with one work on the feature wall and the rest as catalogue rows.
  4. 04The exhibit page inherits the room's wall colour, so opening a work does not change rooms.
  5. 05The same array feeds the sitemap, the JSON-LD, the homepage timeline and the guided tour. None of them holds a second copy.

What a row means

lib/eras.ts → lib/milestones.ts → content/milestones/<slug>.json → /ai-museum/<slug>

The chain runs one way and every consumer reads from the top of it. The alternative, which is what most content sites do, is to let a component declare its own copy of the list so it can render something slightly different. That is how a homepage teaser ends up disagreeing with the index it teases. Here the teaser and the index are the same array sliced differently, so they cannot drift.

Module map

lib/eras.ts
Five eras: year bounds, wall tint, accent. Consumed by the gallery, the sticky signage, the floor plan and every exhibit page. Never redeclared in a component.
lib/milestones.ts
Thirty-one works with a decimal year and an optional parameter count. The optional field is the exhibit; see the decisions below.
content/milestones/
Thirty-one JSON files, each typed against an interface. A missing file is a 404, not a half-rendered page.
components/museum/
EraTimeline, the three navigation components, RoomPlate, ScaleRibbon, Colophon, and the visit-memory store.
lib/seo.ts
The schema.org graph. Article for exhibits, ProfilePage for pioneers, CollectionPage for the museum, breadcrumbs throughout. About 1 KB gzipped per page, measured against production.

The application

The museum floor plan on a near-black background: five gallery rooms drawn as an enfilade of linked cards numbered 01 to 05, each with a small wall-colour swatch, and three side wings beneath them labelled Pioneers, Artifacts and Horizon.
The plan handed to you at the door. Five gallery rooms in sequence with their actual wall colours as swatches, and three wings opening off the corridor beneath. Every room is clickable and reachable by keyboard. It replaced a conventional nav, because a list of links describes a website and a plan describes a building.
A gallery room threshold on a warm off-white wall: an oversized pale numeral 03, the heading The Inflection Era in serif type, a one-line description, the range 2022 to 2023 with seven works, and a photograph of an empty gallery interior captioned Room 03.
Every room opens with a threshold rather than a heading: numeral, name, years and work count, then a photograph of an empty gallery hung as an object on the wall rather than used as a banner. The numeral is the largest thing in the building and carries no information, which is why it is hidden from screen readers.
A logarithmic chart titled The scale of it, plotting disclosed model parameter counts from 65M in 2017 to 2.8T in 2026. Filled markers denote open weights and hollow markers closed models. Short unmarked ticks along the axis mark fifteen milestones that published no figure.
The exhibit is the gaps, not the curve. Filled markers are open weights, hollow are closed models that still published a figure, and the bare ticks along the axis are the fifteen works that published nothing. Series are separated by shape because the two colours measured 1.07:1 apart in luminance and would merge in greyscale.

Decisions

Each era is a room, and the walls change as time advances.

Five wall colours run from aged light brown to near-white, so the Foundation era feels lit and the Agentic era feels clinical. This is the whole argument of the piece delivered without a sentence of copy. It also does structural work: a reader always knows roughly where they are in nine years by the colour of the wall behind them, which no progress bar achieves. Every wall was contrast-checked against every text tone and accent that appears on it, worst case 5.3:1.

An absent parameter count is the exhibit, and is never estimated.

Sixteen of thirty-one works published a parameter count. Fifteen did not. The chart draws the fifteen as unmarked ticks on the axis: present, unmeasured. Filling those with a rumour would have produced a smoother curve and destroyed the only finding the exhibit has, which is that after 2023 every disclosed figure belongs to an open-weight model and not one closed frontier system has stated its size. The measure did not get harder to take. It stopped being offered.

The chart separates its series by shape, not by colour.

Open weights are filled circles, closed models are hollow ones. The two hues chosen first looked clearly distinct and measured only 1.07:1 apart in relative luminance, which means they merge in greyscale and for a red-green colour-blind reader. Colour is decoration here and the shape carries the meaning. The chart also ships a full data table and an aria-label stating the finding in words, because a chart nobody can see should still be readable.

Contrast is computed, never judged.

The dark lobby was the worst offender and looked fine: its muted text measured 4.03:1, and two greys beneath it 2.58:1 and 1.89:1. One pass of a script converting hex to relative luminance and applying the WCAG formula found four failures that no amount of looking had caught. amber-700 was the documented accent for months at 3.93:1 and is now banned project-wide. Nothing here is a matter of taste; it is arithmetic, and it is cheap to run.

Visit memory reads localStorage as an external store.

Tracking which exhibits a reader has seen is the textbook case for useState plus useEffect, and that version is wrong: it renders once with the empty value and then corrects itself, which is a visible flash and a hydration mismatch waiting to happen. useSyncExternalStore models it correctly, and the server snapshot is empty so hydration always agrees. The subtlety is that getSnapshot must return a stable reference when nothing changed. Returning a freshly parsed array each call type-checks, passes review, and re-renders forever.

components/museum/useVisited.tsx
function getSnapshot(): string[] {
  let raw: string | null = null;
  try {
    raw = window.localStorage.getItem(KEY);
  } catch {
    raw = null;
  }
  if (raw !== cachedRaw) {
    cachedRaw = raw;
    try {
      const parsed = raw ? JSON.parse(raw) : [];
      cachedList = Array.isArray(parsed)
        ? parsed.filter((x): x is string => typeof x === "string")
        : EMPTY;
    } catch {
      cachedList = EMPTY;
    }
  }
  return cachedList;
}

Reduced motion needs two mechanisms, because one cannot reach the other.

A CSS prefers-reduced-motion rule cannot stop Framer Motion, which animates via JavaScript transforms and never consults the stylesheet. The View Transitions API does not consult the preference on its own either. So the museum wraps itself in a motion config honouring the user setting, and separately disables the view-transition animations in CSS. Shipping only the CSS half would have looked correct in a code review and done nothing for the reader who asked for it.

A performance win was refused three times, and the refusal is recorded.

content-visibility: auto would genuinely help the thirty-one catalogue rows. It is still not enabled, because those rows reveal via a viewport-triggered animation, and if the browser defers rendering the reveal may never fire and leave a row stuck at zero opacity. That failure is invisible in server HTML and so cannot be caught by the way everything else here is verified. It goes in after somebody watches it in a real browser, and not before.

What it found

4.03:1

The contrast ratio of text that looked perfectly readable.

Found by computing rather than by looking, along with two worse cases at 2.58:1 and 1.89:1 in the same pass. Every text and background pair in the museum now passes WCAG AA. Touch targets went from about 23px to 44px in the same audit, which was also invisible until it was measured.

16 of 31

Works that published a parameter count at all.

The other fifteen are drawn as unmarked ticks rather than omitted or estimated. After 2023, all eight disclosed figures belong to open-weight models. The exhibit is the gap, and it only exists because the data model allows a field to be genuinely absent rather than defaulting it to zero.

43 pages, 3 arrays

Everything is generated from data, nothing is authored twice.

Forty-three routes, the sitemap, the structured data, the homepage timeline and the guided tour all read from the same three TypeScript arrays and forty-two typed JSON files. Adding a work is one array entry plus one file.

What it does not do

  • There is no search and no filter across the thirty-one works. A reader who wants a specific model browses to it or uses the floor plan.
  • Rooms are not their own routes, so an era cannot be deep-linked as a page and the gallery ships as one long document.
  • content-visibility is deliberately not enabled on the catalogue rows, for the reason given above. The performance left on the table is real.
  • English only. Three languages were researched and postponed rather than half-built.
  • The title morph between gallery and exhibit cannot be verified from server HTML, because the transition name is applied by React at transition time. It is built to degrade to a plain navigation where the browser lacks support.
  • The museum is an authority asset, not a funnel. Nobody tours it and then buys anything, and it is not built to make them.

Where it stands

Live at /ai-museum and part of this site. Complete rather than parked: it is extended when the field produces something worth hanging, most recently to bring it current through August 2026, which added nine works and a fifth era.

Every fact in it was researched against primary sources and every citation resolves. Room photography comes from the Pexels API and is credited in a colophon, which is a licence condition of that API rather than a courtesy.