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:
Iandlare both plain bars;gis single-storey. - Plex Sans:
Ihas slab serifs,lhas a tail,gis double-storey. - Source Sans 3:
lhas a slight tail;gis single-storey. - Libre Franklin:
Iandlare both plain bars, like Inter;gis single-storey. - Fira Sans:
lhas a prominent tail;gis double-storey;yandgcarry 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.proseand.rbodycompute toIBM Plex Sansat weight 400;.rnamecomputes toInter Variableat 600..undescribedis italic on--prose.- Both italics and the 600 and 620 weights resolve on demand.
.idpathstill computes toui-monospace, untouched.distfonts: 4 files, about 150KB. No non-latin subset and no legacy.woff.