sprig and prose docs classic docs
Reference› ADRs› ADR005 ProseSans

ADR005 ProseSans

Title: Prose sans for the beta field guide UI

  • Status: Accepted
  • Decided: D2026-08-22
  • Supersedes: ADR001 FontPairing
  • Affects: Ui

Context

ADR 001 split type two ways: IBM Plex Serif for the author's describe text, Inter for everything the tool says about it. The split was the typographic statement of the design — description is the content, structure is scaffolding. Inter for chrome is not in question. Its constraints are unchanged: 11–12px labels, weights 560/600/620, variable, tabular figures, self-hosted, offline.

The serif in the prose slot is. It was chosen because it is a text face, it recedes, it had the tallest x-height of the serifs measured, and it does not blend with Inter. Only one of those properties is "has serifs." After living with the pairing, the serif itself is the part that is not enjoyable to read.

The question this record answers is which sans takes that slot without throwing away the rest of what 001 selected for.

Constraints that still apply: no network (self-hosted latin woff2), a real italic not an oblique (.undescribed is italic), only 400 + italic needed, choose by reading real universes, latin-only hand-declared @font-face.

Two things from 001 expire if prose becomes sans:

  • The classification split. Serif-vs-sans was the loudest available signal that a block of words is the author's rather than the tool's. Two sans families are a quieter seam. Size, colour and measure still help. The glanceable cue gets weaker. That is a real cost.
  • Source Sans 3's rejection (smallest sans x-height, at UI sizes) and Libre Franklin's (no tabular figures). Prose is 16.5px and is never asked for tabular-nums. Both faces were back in contention.

Decision

IBM Plex Sans for prose, Inter for the interface, both self-hosted as latin-only woff2. The monospace face is still the system stack.

The CSS token is --prose, not --serif. Leaving --serif pointing at a sans would be a lie. tokens.css still carries the prototype --serif unused. .note.serif is .note.prose.

The pairing was chosen by reading this package's own universe in each candidate, against Inter as the control, with the same canvas measurements as 001. Chrome stayed Inter for every candidate. Six faces sat behind numbered buttons in the top bar; 0 was Inter.

Consequences

The voice split is quieter. It is now a bookish grotesque against a UI grotesque, plus the existing size split (16.5px describe vs 11–12px labels). Plex Sans has a double-storey g and a serifed I; Inter has a single-storey g and a plain I. On a reading row that is enough to tell whose words you are in: .rname computes to Inter at 600 / 15.5px, .rbody to Plex Sans at 400 / 15px. It is not as loud as serif-vs-sans. That is the cost of this change.

Optical size is unchanged. Plex Sans and the outgoing Plex Serif both measure x-height 52, so at 16.5px the lowercase is 8.58px tall either way. The page's existing rhythm did not need a size nudge.

Cost. Still four font files. Plex Sans's two latin 400 faces are about 47KB against Plex Serif's 40KB. dist fonts land around 150KB. One npm dependency swapped (@fontsource/ibm-plex-sans for @fontsource/ibm-plex-serif). Inter is untouched.

The prose face still needs only two files. Nothing sets it bold. Verified: .prose, .note.prose and .rbody all compute to IBM Plex Sans at weight 400; .undescribed is italic on that family.

Reading was two passes in one session, not several days. 001's method was days of actual use. This pick follows 001's stated criteria — evenness over personality, x-height, distinctness from Inter — on measurements and on reading TypeFaces, the universe overview, and a sequence end to end. Reversibility is still two lines in fonts.css. If a later sitting finds Plex Sans blending with Inter in a way this sitting did not, swap the face; do not reopen chrome.

Latin only, same as 001. Non-latin text falls through to the system stack.

Alternatives considered

Inter for both. The control, and the simplest option: drop two files, one dependency. At 16.5px Inter is a competent reading face. It fails on role. Inter was chosen as a scanner. Putting it in the prose slot collapses the voice split to size and colour. On TypeFaces, h1.title and .prose computed to the same family. Rejected.

Source Sans 3. The better category fit on paper: a humanist text sans, true italics, already measured. Its 001 rejection (weak at 11px) is expired. It recedes. It also measures x-height 49, so at 16.5px the lowercase is 8.08px — 6% smaller than the outgoing serif, the same direction as the Newsreader problem, milder. Compensatable by bumping size to ~17.5px. It also has Inter's single-storey g, so the skeleton difference from chrome is weaker than Plex's. Not chosen: a size change plus a weaker seam, to gain evenness we already have.

Libre Franklin. Editorial gothic warmth as a replacement for the old classification cue. Tabular figures no longer disqualify it. At large size its I and l are both plain bars, same as Inter, and its g is single-storey. The seam is mostly texture. Slightly more personality than Plex. Not chosen.

Fira Sans. The one new face, because the question changed from pairing to prose sans. Spiekermann/Carrois, drawn for screen, true italics. Measures x-height 53, cap 70 (same cap as Plex Serif), the highest x-to-cap ratio in the set at 76%, tabular figures, optical x-height 8.75px. Double-storey g and a tailed l, so it is distinct from Inter. Also a distinctive y and g over long paragraphs — the property that lost Literata in 001. Not chosen: evenness still beats personality for prose read at length.

Atkinson Hyperlegible. The Literata of this round. Purpose-built distinction (I/l/1, slashed 0), static, 400 + italic is the complete prose requirement so its missing odd weights do not matter. Include it to test whether legibility-by-design beats receding-by-taste for paragraphs. It does not. Over a sequence of reading rows the letterforms do not recede. Rejected.

Public Sans, Inter clones. Not in the set. Public Sans is the Charis of this round: sound, not the thing you would miss. Geist, Figtree, Roboto Flex fail the seam by construction.

Keeping a serif. The reason this record exists. Out of scope.

Evidence

Measured in Chrome via canvas TextMetrics, at font-size: 100px, same method as 001. Previously measured faces reproduced 001's numbers exactly, which is why the Fira row can be trusted. "x at 16.5px" is 16.5 * (x-height / 100), the actual lowercase height .prose paints. Plex Serif's 8.58px is the size to match.

Family Role x-height cap x/cap Label width Tabular figures x at 16.5px Italic
Inter Variable control 54 73 74% 1522 yes 8.91px yes
IBM Plex Sans (chosen) candidate 52 71 73% 1474 even by default 8.58px yes
Libre Franklin candidate 53 74 72% 1507 none 8.75px yes
Fira Sans candidate 53 70 76% 1457 yes 8.75px yes
Atkinson Hyperlegible candidate 51 68 75% 1428 yes 8.42px yes
Source Sans 3 candidate 49 66 74% 1350 even by default 8.08px yes
IBM Plex Serif outgoing 52 70 74% 1538 even by default 8.58px yes

Plex Sans is the only candidate whose optical size matches the outgoing serif without a size change. Fira's 76% x-to-cap is the Faustina of this set: drawn to hold at small sizes, more personality than the winner.

Letterforms that settled the seam

Compared at 72px on the same pangram, against Inter:

  • Inter: I and l are both plain bars; g is single-storey.
  • Plex Sans: I has slab serifs, l has a tail, g is double-storey.
  • Source Sans 3: l has a slight tail; g is single-storey.
  • Libre Franklin: I and l are both plain bars, like Inter; g is single-storey.
  • Fira Sans: l has a prominent tail; g is double-storey; y and g carry more personality over a paragraph.
  • Atkinson: every test character is unique, including a slashed 0.

The 001 fear that Plex Sans would blend with Inter is real as a category (two neutrals) and false as letterforms. The bookish g and the serifed I are the remaining voice cue.

Verification of the shipped result

  • Exactly four faces declared, zero load failures.
  • .prose, .note.prose and .rbody compute to IBM Plex Sans at weight 400; .rname computes to Inter Variable at 600.
  • .undescribed is italic on --prose.
  • Both italics and the 600 and 620 weights resolve on demand.
  • .idpath still computes to ui-monospace, untouched.
  • dist fonts: 4 files, about 150KB. No non-latin subset and no legacy .woff.