Readable Type
The smallest size a thing is allowed to be, and what may change when the screen does. Sixteen for body and inputs, fifteen for navigation, twelve as the floor — at every width.
Also called Minimum Font Size, Legibility — in this system all of them are Readable Type.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
The last build reached every production region. Nothing is waiting on approval.
Updated 4 minutes ago · 3 regions
The last build reached every production region. Nothing is waiting on approval.
Updated 4 minutes ago · 3 regions
The floor
Four sizes carry a product. Twelve is the smallest, it is for metadata, and there is nothing under it — the last two rows are on this table so they can be pointed at, not chosen.
| Specimen | Size | What it may carry |
|---|---|---|
| Renews on the 26th | 16px | Body copy and every inputallowed Prose, and any field typed into or read back. Below 16px, iOS zooms the page on focus. |
| Renews on the 26th | 15px | Navigation and UI textallowed Sidebar rows, menus, list rows, tabs. Scanned from the corner of the eye, so it cannot be small. |
| Renews on the 26th | 13px | Supporting textallowed Help text and table cells — text explaining something already on the screen. |
| Renews on the 26th | 12px | Captions and metadataallowed Timestamps, counts, hints. The floor: correct here, and never used for anything a user must read to proceed. |
| Renews on the 26th | 11px | Nothingnever The size a legend, an axis label or a "beta" pill gets talked into. It is information, set below the size it can be read at. |
| Renews on the 26th | 10px | Nothingnever Illegible to a large share of adult readers at arm’s length. If it fits only at 10px, the layout is the problem. |
Desktop, tablet, phone
One row of this table never changes. Everything else is where a responsive design is supposed to do its work — and the reason it never has to touch the reading sizes.
| What | Desktop | Tablet | Phone |
|---|---|---|---|
Body, UI, supporting, caption sizesnever changes | 16 / 15 / 13 / 12 | Same | Same |
Display and h1–h2 | 44 / 32 / 24 | 36 / 28 / 22 | 28 / 24 / 20 |
Measure | 68ch | 62ch | Whatever fits |
Columns | Two or three | Two | One |
Row height / touch target | 32px rows | 40px | 44px minimum |
How much is on screen | All of it | Most | The important part |
The reader’s own setting
The container below stands in for the root element, which is what a browser’s text-size setting actually moves. One column follows it; the other cannot.
Your subscription renews on the 26th.
Your subscription renews on the 26th.
Every reader who has raised this setting did it because the default was too small for them. A px font size is a decision to overrule that, silently, on every screen — and it is invisible to anyone testing at 100%.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Every part, every measurement, and the reason it is that number.
The last build reached every production region, and nothing is waiting on approval.
Updated 4 minutes ago
Three sizes, three jobs, and a row tall enough to hold the first one. Nothing here changes on a phone except the width of the paragraph.
- Navigation text15px / 21px / 470
Read from the corner of the eye by someone deciding where to go next, not by someone already looking at it. One step under body, and never under 15px.
- Row height32px
The line box is 21px; the remaining 11px is what stops a list of destinations reading as a wall. On touch the target grows to 44px without the text changing.
- Body16px / 1.6
The default for prose and for every input. Below 16px an input also makes iOS Safari zoom the page on focus, which leaves the reader scrolled sideways.
- Measure46ch
In ch, so it is still a measure after a size change or a zoom. This is the value that narrows on a phone — the size is not.
- Metadata12px
The floor, and the only size allowed to sit on it. If a caption has to go smaller to fit, the caption is too long or the card is too small.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Color
| Token | Value | Used for |
|---|---|---|
| --ds-fg-secondary | — | Body at 4.5:1 or better |
| --ds-fg-muted | — | Metadata — verified at 12px, never below |
Spacing
| Token | Value | Used for |
|---|---|---|
| row height | Holding a 15px label comfortably |
Typography
| Token | Value | Used for |
|---|---|---|
| --text-body | Body copy and every input | |
| --text-ui | Navigation, menus, list rows | |
| --text-body-sm | Supporting copy | |
| --text-caption | Metadata — the floor | |
| --text-display | The one place responsive sizing belongs |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Height | Type | Max width | Touch target | When to use |
|---|---|---|---|---|---|
| Body | 16px | 400 weight | 68ch | n/a | Prose and every input. The size the product is read at. |
| UI | 15px | 470 weight | 40ch | 44px row on coarse pointers | Navigation, menus, list rows, tabs. |
| Supporting | 13px | 400 weight | 72ch | — | Help text and table cells. Never the only copy explaining a control. |
| Caption | 12px | 400 weight | 60ch | — | Timestamps, counts, hints. The floor. |
| Below 12px | — | — | — | — | Not a size. If something only fits at 11px, cut the string or widen the container. |
Every row in this table was set two steps down so one more row would fit above the fold, which is a trade nobody described out loud.
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- Small text needs more contrast, not less. 12px metadata on --ds-fg-muted is verified at 4.6:1 on --ds-surface; the same colour one step smaller has no verification and no excuse.
- The "large text" exception (18.66px regular, 14px bold, 3:1) exists for headings. Designing ordinary UI text down to it is using an exemption as a target.
- Never let a lighter weight stand in for a smaller size. 400 and 600 of one colour measure identically, so weight cannot rescue text that is too small to resolve.
Keyboard
| ⌘ / Ctrl + | Browser zoom to 200%. The page must reflow without horizontal scrolling at 320px CSS width. |
| Browser text size | Independent of zoom, and the setting rem answers. Set it to 20px and reload before shipping. |
Screen readers
- Text size is invisible to a screen reader and critical to everyone using magnification, which is a much larger group. Do not treat "it is announced correctly" as covering it.
- Never render text as an image to keep it small and sharp. It cannot be resized, selected, translated or read aloud.
- A 200% zoom is the assistive technology most people actually use, and it is built into every browser they already have.
Focus & touch
- Nothing here is focusable on its own, but everything here is what a focused control says. A focus ring around an 11px label is a ring around something the reader still cannot read.
- On a coarse pointer every row is at least 44px, and the text inside it does not change size to pay for that. A 15px label in a 44px row is the correct phone treatment; a 12px label in a 44px row is a big target for something you cannot read.
| Attribute | Applied to | Notes |
|---|---|---|
| font-size | Any text node | In rem. This is an accessibility property before it is a visual one. |
| line-height | Any text block | Unitless, and at least 1.5 for body copy — WCAG 1.4.12 requires that it survive being overridden. |
| meta[name=viewport] | The document | Never user-scalable=no, and never maximum-scale=1. Both disable pinch zoom, which is the only tool some readers have. |
Example usage
1// Text you click is text-ui, in a row tall enough to hold it.2<a className="flex h-8 items-center rounded-md px-2.5 text-ui">Deployments</a>34// Text you read, and text you type into, are both text-body.5<p className="max-w-[68ch] text-body text-fg-secondary">{description}</p>6<input className="h-9 w-full rounded-md px-3 text-body" />78// Metadata is text-caption, and there is nothing below it.9<span className="text-caption text-fg-muted">{relativeTime(updatedAt)}</span>1011// A phone gets the same sizes. What it gets less of is content.12<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">{cards}</div>CSS
The whole standard, as CSS. Four sizes, four units, one media query that does not touch them.
:root {
/* rem, so the reader's own browser text-size setting still applies. */
--text-body: 1rem; /* 16 — prose and every input */
--text-ui: 0.9375rem; /* 15 — navigation and UI text */
--text-body-sm: 0.8125rem;/* 13 — supporting copy */
--text-caption: 0.75rem; /* 12 — metadata. The floor. */
}
/* Line heights are unitless: they multiply against whatever size the
element ends up at, so they survive inheritance and user zoom. */
body { font-size: var(--text-body); line-height: 1.6; }
.nav-item { font-size: var(--text-ui); line-height: 1.4; font-weight: 470; }
/* em only for what is genuinely relative to the current font. */
h2 { letter-spacing: -0.017em; }
/* ch for reading width, because a measure is counted in characters. */
.prose { max-inline-size: 68ch; }
/* Inputs sit at body size. Under 16px, iOS zooms the page on focus. */
input, textarea, select { font-size: var(--text-body); }
/* clamp() belongs to display type and nothing else. */
.hero { font-size: clamp(1.75rem, 1.25rem + 2.2vw, 2.75rem); line-height: 1.1; }
/* The only thing a breakpoint changes about text is how wide it runs. */
@media (max-width: 640px) {
.prose { max-inline-size: 100%; }
/* No font-size here. Not one. */
}Professional tips
- The quickest audit in the codebase: grep for font-size values under 0.75rem, and for any font-size inside a max-width media query. Those two searches find almost every violation of this page.
- When a string only fits at 11px, the string is the problem. Shorten the label, widen the column, or wrap — three fixes that cost nothing, against one that costs a reader.
- Raising a size usually means raising the box around it too. A 15px label needs a 32px row; dropping it into a 24px row trades one readability problem for another.
- Test with the browser text size at 20px, not just at 200% zoom. They are different settings, and only one of them exposes px font sizes.
Performance
- Larger type costs nothing to render. It costs layout — fewer rows above the fold — which is a design decision, not a performance one.
- Holding sizes constant across breakpoints removes a whole class of media queries, and with them the CSS that goes stale the next time the scale moves.
- clamp() is resolved by the browser at layout time with no JavaScript and no resize listener. Where it belongs it is free; the argument against it on body text is never performance.
Common mistakes
- Shrinking type at a mobile breakpoint to keep a desktop layout intact. The layout was the thing that was supposed to change.
- Setting font-size in px, which overrides the reader’s browser setting — the most common accessibility defect in shipped design systems.
- Treating 12px as a general small size rather than as the floor for metadata. It is the last step, not a spare one.
- Using user-scalable=no in the viewport meta to stop iOS zooming a 14px input. Fix the input size; disabling pinch zoom takes the workaround away from the reader too.
Real-world recommendations
- Hold the phone at the distance you actually hold a phone — not the distance you hold it when checking your work — and read the smallest string on the screen. That is the test.
- Ask someone over fifty to read the densest screen in the product. It takes two minutes and it will find things no contrast checker reports.
- Sizes chosen on a 27-inch display at 100% scaling are chosen roughly 30% larger than they will be seen on a laptop at default scaling. Check on the smaller machine before deciding something can go down a step.
- When a stakeholder asks for more on the screen, offer fewer columns or a denser layout first. "We will shrink the text" is the answer that sounds cheapest and is not.