Michael Pham.
Tech notes

12 July 2026 · 8 min read · stable

Follow the white rabbit: how this site was built

#nextjs#tailwind#webgl#design

This site is a static Next.js 16 app styled with Tailwind CSS 4, wearing the Matrix as a skin. The short version of every design decision lives in the colophon (linked in the footer); this note is the longer version — what's actually under the hood, and what surprised me building it.

The most important decision happened before any Matrix theming: token-first color. The entire site reads from five CSS variables — paper, ink, a soft ink, a line color, and an accent — defined in oklch and swapped wholesale by a data-theme attribute on the html element. This site went through five visual identities on the way here (warm editorial, an ember-industrial phase, field notes, two shades of green), and each rebrand cost roughly one block of tokens. oklch matters more than it looks: its lightness is perceptually uniform, so a palette of greens can share brightness levels and the hierarchy survives the theme swap. A tiny inline script in the head applies the stored theme before first paint, so there's no flash of the wrong world.

Typography carries the concept: JetBrains Mono for anything the machine would say — headings, labels, metadata — and Newsreader, a serif built for long-form reading, for anything I say. The mono display font also quietly enables the scramble effect: cipher characters have the same width as the letters they replace, so text can decode without layout shift.

The travel globe is cobe, a ~5KB WebGL globe. The surprise: cobe 2.0 removed the onRender callback that every tutorial on the internet uses. In v2, createGlobe renders one frame and hands you { update, destroy } — you drive the animation yourself with your own requestAnimationFrame loop calling update({ phi }). My first version rendered a beautiful, perfectly frozen globe. Read the type definitions of the package you actually installed, not the one in your head.

Dragging the globe in two axes is just phi and theta with the tilt clamped so you can't flip the planet, plus a decaying velocity for momentum. One lesson from user testing (n=1): vertical drag must move the surface with the cursor, not the camera — I shipped it inverted first, and it felt wrong within seconds.

The city labels are the part I'd show off. cobe v2 gives each marker an id and maintains invisible anchor elements at each marker's projected screen position, exposing them through CSS anchor positioning (position-anchor: --cobe-tokyo) plus a --cobe-visible-* custom property. Labels are plain spans that track a spinning WebGL globe with zero per-frame JavaScript — and the visibility trick is delightfully cursed: the property is set to an invalid value when the marker faces you, which makes the opacity declaration fall back to its initial value of 1. CSS as a boolean channel.

The digital rain and the cursor glyph trail are both plain 2D canvas. The rain uses the classic fade trick — each frame paints the background color at low alpha instead of clearing, so glyphs leave phosphor trails — with the fill color read from the paper token so it works in both themes. The cursor trail is deliberately throttled: one glyph per ~28 pixels of travel with a 30ms floor, and its render loop stops entirely when the last particle dies. Ambience shouldn't cost battery while you read.

The rest is restraint dressed as theatrics. The scanlines are one fixed repeating-linear-gradient overlay, dark mode only. The typewriter and scramble are setTimeout state machines. Everything respects prefers-reduced-motion — the rain freezes into a still frame, the typewriter just prints, the scramble skips — and every overlay is pointer-events: none, so no effect can ever intercept a click. No analytics, no trackers.

If I could hand one lesson back to myself at v1: pick the boring architecture (tokens, static generation, data files) before the fun one, because the boring layer is what makes the fun layer cheap to change. The Matrix skin took an afternoon. It took five afternoons to earn an architecture where it only took one.

// end of transmission

Next up

Airflow patterns that survive 300+ pipelines