How this site is built
Static HTML, two variable fonts, and JavaScript only where a figure needs it.
Most of this page writes itself. Version numbers, file sizes, contrast ratios and the motion curves are read from the site’s own source when it is built, so they describe this build and not some earlier one.
Constraints
Every page is static HTML, rendered before it is deployed. A page with nothing to operate ships no JavaScript files. Pages with working figures load React when the first figure scrolls into view, and not before; the footer of each page says which kind you are on.
Both themes, light and dark, are checked against WCAG 2.2 AA. Nothing moves until you ask it to, and with reduced motion turned on, figures change state without animating.
Pages without JavaScript
The site is built with Void 1.1.11. Essays and lab plates are markdown, with their figures written as React components and embedded as islands; the index pages and this colophon are TSX. All of it is prerendered. There is no client-side router: links are ordinary anchors, each one loads a new document, and where the browser supports cross-document view transitions, the running head and entry numbers carry over from page to page.
Fig. 1 · from source to served files
What the build does. Source pages go through Void’s prerender and the site’s build plugins. Out come 16 HTML pages (one of them the not-found page), one stylesheet shared by all of them, three font files, and JavaScript only for pages with islands.
The build plugins do the plumbing. One attaches each markdown file’s front matter to its page, one tidies the markup @void/md generates, one gathers every component’s CSS into the single stylesheet, and one writes the RSS feed, the sitemap and the social cards, then runs the checks described below.
What figures cost
Each row is one page as a first-time visitor downloads it, with every figure on it loaded. HTML, CSS and JavaScript are counted compressed with gzip, as they travel; fonts are counted as stored. Measured on with pnpm qa:measure.
| Page | HTML | CSS | Fonts | JavaScript | Total |
|---|---|---|---|---|---|
| / | 5.8 kB | 29.8 kB | 79.0 kB | 0 B | 114.6 kB |
| /colophon | 13.2 kB | 29.8 kB | 79.0 kB | 0 B | 122.1 kB |
| /lab | 6.6 kB | 29.8 kB | 79.0 kB | 0 B | 115.4 kB |
| /lab/ | 7.1 kB | 29.8 kB | 79.0 kB | 82.8 kB | 198.8 kB |
| /lab/ | 7.2 kB | 29.8 kB | 79.0 kB | 79.3 kB | 195.4 kB |
| /lab/ | 6.6 kB | 29.8 kB | 79.0 kB | 81.3 kB | 196.7 kB |
| /lab/ | 7.7 kB | 29.8 kB | 112.6 kB | 79.9 kB | 230.0 kB |
| /lab/ | 6.0 kB | 29.8 kB | 79.0 kB | 0 B | 114.8 kB |
| /lab/ | 6.8 kB | 29.8 kB | 79.0 kB | 80.4 kB | 196.1 kB |
| /lab/ | 8.2 kB | 29.8 kB | 79.0 kB | 82.5 kB | 199.6 kB |
| /lab/ | 8.5 kB | 29.8 kB | 79.0 kB | 81.8 kB | 199.2 kB |
| /projects | 9.7 kB | 29.8 kB | 79.0 kB | 0 B | 118.6 kB |
| /writing | 3.9 kB | 29.8 kB | 79.0 kB | 0 B | 112.8 kB |
| /writing/ | 15.9 kB | 29.8 kB | 79.0 kB | 88.0 kB | 212.8 kB |
| /writing/ | 18.6 kB | 29.8 kB | 112.6 kB | 91.1 kB | 252.2 kB |
On pages with figures, React and the figure code come to 79.3 kB to 91.1 kB of JavaScript once every figure has loaded. Every other page loads no JavaScript files.
The footer also measures the page you are reading, in your own browser: open “Measured here” for the file-by-file list. Files your browser already holds in memory report zero bytes and are listed as cached.
Two typefaces, three files
Literata sets everything written as prose: titles, body text and summaries. Recursive sets the interface and every literal value. Its MONO axis runs from a sans, used for labels and buttons, to a monospace, used for numbers, dates, paths and code, so one file covers both registers.
Each file is subset to the 213 characters the site allows and keeps only the variable axes the design needs; the others are pinned. A character outside the set would be drawn in some other typeface, so the build warns about any it finds. Arrows, ticks and crosses on this site are drawn as SVG for the same reason.
| File | Role | Axes | Size | Loading |
|---|---|---|---|---|
| literata-roman.woff2 | Reading, titles | wght 400–700 | 34.4 kB | preloaded |
| literata-italic.woff2 | Emphasis | wght 400–600 | 33.6 kB | on demand |
| recursive.woff2 | Interface, values and code | MONO 0–1, wght 400–700; CASL 0, slnt 0, CRSV 0.5 pinned | 44.6 kB | preloaded |
On a first visit the two preloaded files come to 79.0 kB. The italic is fetched only by pages that set italic text. Both families are released under the SIL Open Font License, version 1.1: read the licences for Literata and Recursive.
Before the fonts arrive, text is set in local fallback faces adjusted to Literata’s and Recursive’s metrics, so when the real faces swap in, lines barely move. The overrides were computed from the subset files:
@font-face {
font-family: "Literata Fallback Georgia";
src: local("Georgia");
ascent-override: 109.3141%;
descent-override: 28.6056%;
line-gap-override: 0%;
size-adjust: 107.6714%;
}
@font-face {
font-family: "Recursive Fallback Arial";
src: local("Arial"),
local("ArialMT"),
local("Helvetica"),
local("Liberation Sans"),
local("LiberationSans"),
local("Arimo");
ascent-override: 83.0413%;
descent-override: 21.853%;
line-gap-override: 0%;
size-adjust: 114.4009%;
}
@font-face {
font-family: "Recursive Mono Fallback Courier";
src: local("Courier New"),
local("CourierNewPSMT"),
local("Liberation Mono"),
local("LiberationMono"),
local("Cousine");
ascent-override: 95.0155%;
descent-override: 25.0041%;
line-gap-override: 0%;
size-adjust: 99.9837%;
}Colour, checked
The palette is a short list of tokens: warm paper, graphite ink, a few inks kept for figures, and one eucalyptus green, kept for the things you can operate, the current value in a figure and the AI marker. Each foreground is checked against every surface it can sit on, in both themes, and pnpm check:tokens fails if any pair drops below its minimum. Text needs 4.5:1. Control borders need 3:1.
Fig. 2 · contrast on every surface
| Foreground | paper | sheet | inset | wash |
|---|---|---|---|---|
| inktext 4.5:1 | 15.31AAA | 16.08AAA | 14.24AAA | 13.72AAA |
| ink-2 | 7.84AAA | 8.23AAA | 7.29AAA | 7.02AAA |
| ink-3 | 5.56AA | 5.83AA | 5.17AA | 4.98AA |
| signal | 5.30AA | 5.57AA | 4.93AA | 4.75AA |
| signal-hi | 7.42AAA | 7.80AAA | 6.91AA | 6.65AA |
| ochre | 5.44AA | 5.71AA | 5.06AA | 4.87AA |
| rust | 5.43AA | 5.70AA | 5.05AA | 4.87AA |
| code-fn | 7.24AAA | 6.42AA | ||
| code-str | 6.45AA | 5.72AA | ||
| code-num | 6.25AA | 5.53AA | ||
| rule-strongUI 3:1 | 3.37UI | 3.54UI | 3.13UI | 3.02UI |
| ruledecorative | 1.30– |
| Foreground | paper | sheet | inset | wash |
|---|---|---|---|---|
| inktext 4.5:1 | 15.06AAA | 14.22AAA | 15.76AAA | 10.89AAA |
| ink-2 | 9.28AAA | 8.76AAA | 9.71AAA | 6.71AA |
| ink-3 | 6.46AA | 6.10AA | 6.76AA | 4.67AA |
| signal | 9.61AAA | 9.07AAA | 10.05AAA | 6.95AA |
| signal-hi | 12.51AAA | 11.81AAA | 13.08AAA | 9.04AAA |
| ochre | 9.78AAA | 9.23AAA | 10.23AAA | 7.07AAA |
| rust | 7.14AAA | 6.74AA | 7.46AAA | 5.16AA |
| code-fn | 9.38AAA | 10.40AAA | ||
| code-str | 9.23AAA | 10.23AAA | ||
| code-num | 7.24AAA | 8.02AAA | ||
| rule-strongUI 3:1 | 4.28UI | 4.04UI | 4.47UI | 3.09UI |
| ruledecorative | 1.41– |
Every pair the check asserts, light and dark, and the decorative rule, which it records but does not check. Each ratio is printed in the foreground it measures, on the surface it measures; border colours, which never set text, are shown as a line. Empty cells are pairs the design never uses.
Motion that asks first
Motion is defined as tokens too: four cubic Bézier easings and three springs. Each spring is simulated from its stiffness, damping and mass, then compiled into a CSS linear() easing, so the same spring can drive a CSS transition or a figure’s own animation.
Fig. 3 · motion tokens
The motion tokens. Easings are sampled from their cubic Bézier values. Springs are drawn from the model that generates their CSS, and their durations and overshoot are computed from it.
Durations stay short: colour changes on hover take 160 ms, interface transitions 240 ms, and figure marks 400 ms.
If your system asks for reduced motion, nothing slides, scales or draws itself in. Figures jump straight to their new state, simulations advance a step at a time, page-to-page transitions are switched off, and only brief colour fades remain. Whatever the setting, no figure moves on its own.
Checked on every build
pnpm build ends by checking the HTML it has just written, and stops on any of these:
- a link to one of my GitHub repositories outside the allowlist of 23 public names, or an email address anywhere in the output;
- a footer that says “no JavaScript files” on a page that loads a script, or an island count that does not match the page;
- an island inside a paragraph, a link that opens a new tab, or one view-transition name used twice on a page;
- a page without exactly one title, description and h1, a canonical link that does not match the page, or a language other than en-AU;
- two entries with the same number.
Two checks only warn, so a stray character never blocks a deploy: characters outside the font subsets, and files over budget. The budgets are 30.0 kB for the stylesheet, 14.0 kB for an index page and 30.0 kB for an essay or plate, all compressed, and 60.0 kB for a social card.
Outside the build, a browser-driven suite runs axe on every page in both themes and walks key pages with the keyboard, checking each stop for a visible focus ring. It also looks for sideways scrolling at phone and desktop widths, loads every page with JavaScript off, and repeats the motion checks with reduced motion on. Unit tests cover the colour maths, the spring compiler and the repository list, and they assert the numbers quoted in prose. The page weights are measured rather than asserted: pnpm qa:measure writes them.
Reading this site
A few conventions recur in the essays and the lab.
live numbers
linked terms
transport
entry numbers
construction
Versions
- void
- 1.1.11
- @void/react
- 1.1.11
- @void/md
- 1.1.11
- react
- 19.3.1
- vite-plus
- 1.3.0
- built
- commit
- d95ece6
Published
More writing
Nº 002Next
Your eval score is a distribution
A benchmark number is one draw from a spread. Four figures on reading it.