Sam Joyce

How this site is built

Static HTML, two variable fonts, and JavaScript only where a figure needs it.

Contents (9)
  1. Constraints
  2. Pages without JavaScript
  3. What figures cost
  4. Two typefaces, three files
  5. Colour, checked
  6. Motion that asks first
  7. Checked on every build
  8. Reading this site
  9. Versions

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.

PageHTMLCSSFontsJavaScriptTotal
/5.8 kB29.8 kB79.0 kB0 B114.6 kB
/colophon13.2 kB29.8 kB79.0 kB0 B122.1 kB
/lab6.6 kB29.8 kB79.0 kB0 B115.4 kB
/lab/fan-out7.1 kB29.8 kB79.0 kB82.8 kB198.8 kB
/lab/one-hue7.2 kB29.8 kB79.0 kB79.3 kB195.4 kB
/lab/optical-centre6.6 kB29.8 kB79.0 kB81.3 kB196.7 kB
/lab/quorum7.7 kB29.8 kB112.6 kB79.9 kB230.0 kB
/lab/rag6.0 kB29.8 kB79.0 kB0 B114.8 kB
/lab/scrub-number6.8 kB29.8 kB79.0 kB80.4 kB196.1 kB
/lab/spring-to-linear8.2 kB29.8 kB79.0 kB82.5 kB199.6 kB
/lab/wake-on-message8.5 kB29.8 kB79.0 kB81.8 kB199.2 kB
/projects9.7 kB29.8 kB79.0 kB0 B118.6 kB
/writing3.9 kB29.8 kB79.0 kB0 B112.8 kB
/writing/nothing-else-moves15.9 kB29.8 kB79.0 kB88.0 kB212.8 kB
/writing/your-eval-score-is-a-distribution18.6 kB29.8 kB112.6 kB91.1 kB252.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.

FileRoleAxesSizeLoading
literata-roman.woff2Reading, titleswght 400–70034.4 kBpreloaded
literata-italic.woff2Emphasiswght 400–60033.6 kBon demand
recursive.woff2Interface, values and codeMONO 0–1, wght 400–700; CASL 0, slnt 0, CRSV 0.5 pinned44.6 kBpreloaded

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:

css
@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

38 checked pairs × 2 themes
Light
Foregroundpapersheetinsetwash
inktext 4.5:115.31AAA16.08AAA14.24AAA13.72AAA
ink-27.84AAA8.23AAA7.29AAA7.02AAA
ink-35.56AA5.83AA5.17AA4.98AA
signal5.30AA5.57AA4.93AA4.75AA
signal-hi7.42AAA7.80AAA6.91AA6.65AA
ochre5.44AA5.71AA5.06AA4.87AA
rust5.43AA5.70AA5.05AA4.87AA
code-fn7.24AAA6.42AA
code-str6.45AA5.72AA
code-num6.25AA5.53AA
rule-strongUI 3:13.37UI3.54UI3.13UI3.02UI
ruledecorative1.30–
Dark
Foregroundpapersheetinsetwash
inktext 4.5:115.06AAA14.22AAA15.76AAA10.89AAA
ink-29.28AAA8.76AAA9.71AAA6.71AA
ink-36.46AA6.10AA6.76AA4.67AA
signal9.61AAA9.07AAA10.05AAA6.95AA
signal-hi12.51AAA11.81AAA13.08AAA9.04AAA
ochre9.78AAA9.23AAA10.23AAA7.07AAA
rust7.14AAA6.74AA7.46AAA5.16AA
code-fn9.38AAA10.40AAA
code-str9.23AAA10.23AAA
code-num7.24AAA8.02AAA
rule-strongUI 3:14.28UI4.04UI4.47UI3.09UI
ruledecorative1.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

4 easings, 3 springs
--ease-outhover, colour, small UI
--ease-in-outstate swaps, view-transition morphs
--ease-expofigure entrances, settling marks
--ease-exitexits, at most 160 ms
--spring-settle449 ms, 0.37% overshoot
--spring-detent451 ms, 4.53% overshoot
--spring-soft762 ms, 3.54% overshoot
Each plot runs from start to finish, with the target value as a dashed line. A spring runs for its own settle time and may pass the target before it settles; an easing takes whatever duration it is given.

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

A number with a dashed underline is live: drag it, or focus it and use the arrow keys. Its figure follows.

linked terms

Shapes in the text match shapes in the figure. Hover a term to find its marks.

transport

Figures that move never start on their own. Step, Play, speed and Reset sit under the stage.

entry numbers

Every entry has a number in one sequence across essays, lab plates and this colophon, so a list of one kind skips the numbers of the others.

construction

Show construction draws how the page is made: column tracks, measure, type specs.

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.

essay · 4 figures