TYPEFOUND

Sign in

Back to the home page

This is for the people who run the site.
The typefaces are on sale to everyone.

← The journal

Type in an interface

Interface text is small, read in a hurry, and often a single word. The rules for buttons, form fields, error messages and numerals are not the rules for articles.

29 August 2026 · 9 min read · Perhapxin

Almost every piece of typographic advice ever written assumes a reader who has settled in. There is a measure, there are paragraphs, there is context on either side of any word that goes wrong, and the reader has agreed to spend a few minutes.

An interface has none of that. The text is small, it is read while the reader is looking for something else, and it is frequently a single word with nothing around it to repair a misreading. The failure mode is also different: a badly set paragraph costs you a compliment, and a badly set form field costs somebody a transaction.

What follows is the part of typography that only applies to interfaces, with the numbers attached, because an interface decision that cannot be checked is a decision somebody will quietly undo.

A display face has exactly one address in an interface

We draw eight display faces. Each is a single weight with no italic, and most cover 105 characters — English only. They were drawn to be seen at sixty pixels and they are good at that.

They do not belong in a form field. They do not belong in a button, a tooltip, a table header, a validation message or a navigation item. This is not modesty; it is what the drawings are for, and a foundry that will not say so is not much use to you.

The one place a display face earns its keep is the product's own name — the wordmark in the header, set once, at a size where the drawing is actually visible. That string is not read under time pressure by somebody hunting for the Continue button. Everything else in the chrome is text-face work.

The 105-character figure is its own argument. An interface eventually needs a curly apostrophe, a degree sign, a euro symbol, a surname with a diacritic in it. A poster never has to handle input it did not choose.

A button label is one word doing a great deal of work

Sentence case — Save changes — is our default. It is the fastest to scan, because ascenders and descenders give a lower-case word a distinctive outline and an all-caps word a rectangle. Title case (Save Changes) is defensible and mostly a house decision; what matters is that a product picks one and does not mix them in the same row of buttons.

ALL CAPS is a real option for small labels, and it comes with a correction that is not optional. Every typeface is spaced for its lower case, so uppercase set at the face's default tracking is too tight. Open it up by 0.05em to 0.1em — a working range, adjusted by eye, never in pixels, because the correction has to scale with the size.

Default tracking Continue
letter-spacing: 0.08em Continue
The same word at the same size. Uppercase at a face's default spacing reads as a clot; the corrected version reads as a label.
.btn {
	font: inherit;
	font-size: 16px;
	line-height: 1.25;
	padding: 12px 20px; /* 20px of text + 24px = a 44px target */
}

.btn--caps {
	text-transform: uppercase;
	letter-spacing: 0.08em;
	/* uppercase has no descenders, so an even box looks bottom-heavy */
	padding-block: 13px 11px;
}

A button label is also the worst possible home for a face with ambiguous letterforms. In running text a misread letter is repaired by the word around it. On a button there is no word around it.

Form fields contain a hard number, and it is 16px

Safari on iOS zooms the viewport when a text input, textarea or select is focused and its computed font size is below 16px. At 16px or larger it does not. This is an accessibility behaviour rather than a bug, and the fix is the font size — not maximum-scale=1, which disables pinch zoom for everybody and fails WCAG.

input, select, textarea {
	font-size: 16px;
}

If 16px is larger than the design wants on a desktop, scope the smaller size to fine pointers rather than shrinking it everywhere:

input, select, textarea { font-size: 16px; }

@media (pointer: fine) {
	input, select, textarea { font-size: 14px; }
}

The second thing about form fields is that they do not inherit your font. Every browser's user-agent stylesheet gives input, textarea, select and button their own family and size from the operating system, so a page can be set entirely in one face and still render every field in something else. You have to ask:

button, input, select, textarea {
	font: inherit;
	letter-spacing: inherit;
}

/* font: inherit also pulls in the inherited size, so re-assert the floor */
input, select, textarea { font-size: 16px; }

Order matters there. font is a shorthand, and font: inherit will happily undo a 16px rule written above it.

A placeholder is not a label

A placeholder vanishes the moment somebody types. Anything it was carrying — the format of the date, the fact that the field wanted a country code, the word saying what the field was — vanishes with it, exactly when the user wants to check their work.

A placeholder is a hint, and a label is a promise. If the only thing naming a field disappears when the field is used, that field is unlabelled. Use a visible <label> and keep the placeholder for an example of the format.

Placeholder contrast is the other half. Browsers default to roughly #A9A9A9, which is about 2.3:1 against white — well under the 4.5:1 that WCAG 1.4.3 asks of body-sized text. Whether placeholders are formally exempt from that criterion is argued about; the argument does not help a reader who cannot see the hint. #767676 on white measures 4.54:1 and costs nothing.

::placeholder {
	color: #767676;
	opacity: 1; /* Firefox applies an opacity of its own; this removes it */
}

Error and helper text must not depend on colour

Helper and error text is the smallest type most interfaces contain, and it is the type most likely to be read by somebody already annoyed. Do not set it below 14px, and do not set it in a lighter weight to make it recede — that costs contrast, which is the one thing it cannot spare.

It has to clear 4.5:1 against its background like any other text, and red on white is the usual offender: #FF0000 on white is 4:1 and fails. #C81E1E and darker passes.

Colour is also not allowed to be the only signal. WCAG 1.4.1 is a Level A requirement, and a red border with nothing else changed is invisible to a screen reader and unreliable for a colour-blind user. Pair every error with words and a non-colour mark — an icon, a rule, a changed border width — and put the specific problem in the sentence rather than writing "Invalid input".

Numbers that update in place need tabular figures

Most text faces ship proportional figures, where 1 is narrower than 8. That is correct inside a sentence and wrong anywhere a number changes while the reader is looking at it. A countdown going from 0:11 to 0:08 gets wider, and the whole row shuffles sideways.

Proportional 1:11
8:88
tabular-nums 1:11
8:88
Two lines of the same character count. On the left the digits have different widths, so a timer ticking between them moves everything beside it. On the right they do not.
.price, .timer, .counter,
td[data-numeric] {
	font-variant-numeric: tabular-nums;
	font-feature-settings: 'tnum';
}

This only works if the font actually contains tabular figures — a browser cannot invent them. If yours does not, set the number in a monospaced face or give its container a fixed width. Timers, prices, countdowns, vote counts, file sizes and anything inside a live region all qualify.

Some characters look like other characters

0 and O. 1, l and I. And rn at small sizes closing up into m. In prose these almost never matter, because the word carries the letter. In an interface they turn up precisely where there is no word: one-time codes, order references, licence keys, API tokens, addresses, and passwords somebody is copying from a screenshot.

Typefound Relief 0O 1lI rn m
Text face 0O 1lI rn m
Relief is an extruded slab drawn for headlines, and at poster size the extrusion is the whole point. Here it fills the counter of the zero and thickens the gap in rn. This is a face doing its job in the wrong room.

Three things help, in descending order of how much. Choose a face with disambiguated forms for these fields — a slashed or dotted zero, a serifed 1, a tailed l. Set the field in a monospaced stack, which does not solve the confusion but does make a missing or extra character obvious. And where you generate the code yourself, do not use the ambiguous characters at all: Crockford's base32 alphabet omits I, L, O and U for exactly this reason.

.code {
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas,
		'Liberation Mono', monospace;
	font-variant-numeric: slashed-zero;
	letter-spacing: 0.08em; /* a code is read character by character */
}

Line-height stays tight; the thing you press does not

Interface text is short, so the generous 1.6 that a paragraph wants makes a two-line label look like two separate labels. Use about 1.25 for single-line labels and buttons, and 1.5 for helper text and anything likely to wrap to three lines or more.

Keep a button's height in its padding rather than its line-height. A line-height change alters the box differently from face to face; padding is the same number everywhere.

Then check that box. WCAG 2.2 SC 2.5.8 (Level AA) asks for 24×24 CSS pixels, or 24px of clear spacing around a smaller target. Apple's guidance is 44×44pt, Material's is 48×48dp, and WCAG's Level AAA criterion 2.5.5 is 44×44. A 16px label at line-height 1.25 is 20px tall, so 12px of padding above and below makes 44px exactly, which clears all of them.

The system font stack is usually the right answer

For an interface — as opposed to the marketing pages in front of it — the system stack is hard to beat. It weighs nothing, it never swaps, it is already hinted for the screen it is rendering on, and it matches the rest of the device the person is holding. Written out and current:

:root {
	--ui-sans: system-ui, -apple-system, 'Segoe UI', Roboto,
		'Helvetica Neue', Arial, sans-serif;
	--ui-mono: ui-monospace, SFMono-Regular, Menlo, Consolas,
		'Liberation Mono', monospace;
}

body { font-family: var(--ui-sans); }

system-ui resolves to San Francisco on macOS and iOS, Segoe UI Variable on Windows 11, and Roboto on stock Android. -apple-system is kept for older Safari versions that predate the keyword, and the named families after it cover the cases where system-ui resolves to something unexpected. This is, near enough, what Tailwind and Bootstrap 5 ship as their default.

Spend the font budget where it is seen: the header, the hero, the wordmark. The rest of an interface is a set of small decisions about size, spacing, contrast and figures — and a reader who never notices any of them is the correct outcome.

More from the journal

These notes are written while drawing the typefaces this site sells. They are display faces, licensed for commercial work, from $3.