Typography
Fourteen named styles, one variable family, four units, and a floor nothing readable goes under. Typography is ninety-five percent of an interface — everything else is arrangement.
Also called Text, Heading, Type Scale — in this system all of them are Typography.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
| Specimen | Token | Size / LH / Tracking |
|---|---|---|
| The quick brown fox | text-display | 44px / 1.08 / -0.028em |
| The quick brown fox | text-h1 | 32px / 1.16 / -0.022em |
| The quick brown fox | text-h2 | 24px / 1.25 / -0.017em |
| The quick brown fox | text-h3 | 19px / 1.35 / -0.012em |
| The quick brown fox | text-h4 | 16px / 1.45 / -0.006em |
| The quick brown fox | text-body-lg | 17px / 1.62 / -0.004em |
| The quick brown fox | text-body | 16px / 1.6 / -0.002em |
| The quick brown fox | text-ui | 15px / 1.4 / -0.001em |
| The quick brown fox | text-body-sm | 13px / 1.6 / 0 |
| The quick brown fox | text-label | 13px / 1.2 / 0.001em |
| The quick brown fox | text-label-sm | 12px / 1.2 / 0.004em |
| The quick brown fox | text-caption | 12px / 1.45 / 0.002em |
| Section label | text-overline | 12px / 1.2 / 0.09em |
| The quick brown fox | text-code | 13px / 1.65 / -0.005em |
Measure
Line length is the single biggest lever on reading comfort. 45–75 characters is the usable band; 60–70 is the target.
A line that runs too long forces the eye to travel back across the page and re-find the start of the next line. Somewhere around forty-five characters the return sweep becomes reliable, and somewhere past eighty it stops being.
A line that runs too long forces the eye to travel back across the page and re-find the start of the next line. Somewhere around forty-five characters the return sweep becomes reliable, and somewhere past eighty it stops being.
A line that runs too long forces the eye to travel back across the page and re-find the start of the next line. Somewhere around forty-five characters the return sweep becomes reliable, and somewhere past eighty it stops being.
Units
rem for size, unitless for leading, em only for what is genuinely font-relative, ch for reading width. Four rules, and every one of them is about surviving a change nobody told you about.
| Unit | Use it for | Example |
|---|---|---|
rem | Every font size, without exception. Relative to the root, so a reader who sets their browser text to "large" gets a larger app. A px font size ignores that setting outright — it is the single most common accessibility defect in shipped design systems. | font-size: 1rem; |
unitless | Every line height. A ratio multiplies against whatever size the element ends up at, so one value stays correct through inheritance and through zoom. `line-height: 24px` freezes the leading and clips descenders the moment the text grows. | line-height: 1.6; |
em | Measurements that are genuinely relative to the current font: letter-spacing, and optical nudges of an inline icon. Tracking is a proportion of the letterform, not a fixed distance. −0.02em stays right at 15px and at 44px; −0.7px is wrong at both ends of that range. | letter-spacing: -0.017em; |
ch | The width of anything containing readable text. A measure is counted in characters, so state it in characters. 68ch stays 68 characters through a size change, a zoom, or a locale with wider glyphs; a 640px column silently becomes 90 characters the moment the type grows. | max-inline-size: 68ch; |
Across screens
Navigation stays 15px, body stays 16px, metadata stays 12px — at 1440px, at 834px and at 390px. Only display sizes scale, and only with clamp().
One column becomes two. The measure narrows from 68ch to whatever fits. The hero drops from 44px to 28px because at 390px it would otherwise be four words per line. The paragraph you are reading does not move.
Hierarchy
The left block varies size, weight and colour together. The right varies only size. Both have the same information; only one of them can be scanned.
Numerals
Tabular figures are mandatory anywhere a number can change or be compared vertically. Proportional figures are for prose.
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.
useId()Every part, every measurement, and the reason it is that number.
Deployment history
Every build that reached a production region in the last thirty days, newest first.
Updated 4 minutes ago
A heading, its supporting paragraph, and its metadata. Three styles, three colours, three spacing values — none of them chosen by eye.
- Heading size24px · text-h2
Roughly 1.6× the body size. Steps smaller than 1.25× do not read as a different level; steps larger than 2× read as a different page.
- Heading tracking−0.017em
Negative and proportional to size, which is why it is in em rather than px. Without it, a 24px heading in Inter looks loose next to 16px body text set at its natural spacing.
- Heading → body gap8px
Smaller than the line height of either. Proximity binds them into one unit; a 24px gap would make the paragraph look unrelated.
- Measure62ch
Set in ch so it tracks the font size. A pixel max-width silently becomes the wrong measure the moment the type scale changes.
- Metadata12px, --ds-fg-muted
Two steps down and two colour steps back. Both together, because size alone in a dense UI is not enough separation.
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 | — | Headings |
| --ds-fg-secondary | — | Body |
| --ds-fg-muted | — | Captions and metadata |
Spacing
| Token | Value | Used for |
|---|---|---|
| heading → body | Binding a heading to its paragraph | |
| section gap | Separating major sections |
Typography
| Token | Value | Used for |
|---|---|---|
| --font-sans | All interface text | |
| --font-mono | Code, tokens, IDs, tabular data | |
| --text-display | Marketing hero | |
| --text-h1 | Page title | |
| --text-h2 | Section heading | |
| --text-h3 | Subsection, dialog title | |
| --text-h4 | Card title | |
| --text-body-lg | Lead paragraph, long-form | |
| --text-body | Body copy and every input | |
| --text-ui | Navigation, menus, list rows | |
| --text-body-sm | Supporting copy, table cells | |
| --text-label | Buttons, form labels, tabs | |
| --text-caption | Metadata — the floor | |
| --text-overline | Eyebrows, uppercase only |
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 | When to use |
|---|---|---|---|---|
| Display | 44px | 620 weight | 18ch | Marketing hero only. Never inside the product UI. |
| H1 | 32px | 620 weight | 28ch | One per screen, in the page header. |
| H2 | 24px | 600 weight | 40ch | Major sections within a page. |
| H3 | 19px | 600 weight | 48ch | Subsections and dialog titles. |
| H4 | 16px | 600 weight | 56ch | Card titles and group headings. |
| Body large | 17px | 400 weight | 68ch | Documentation and long-form reading. |
| Body | 16px | 400 weight | 68ch | Prose, and every field the user types into. |
| UI | 15px | 470 weight | 40ch | Navigation, menus, list rows. Never shrinks on a phone. |
| Body small | 13px | 400 weight | 72ch | Supporting copy: help text, table cells, dense panels. |
| Caption | 12px | 400 weight | 60ch | Timestamps, counts, hints. The floor — nothing readable goes under it. |
Sixty-eight characters is roughly eleven words per line, which is close to the span the eye can track without a conscious return sweep.
Section heading
Its paragraph, close beneath.
Your subscription will renew automatically on the 26th unless cancelled beforehand
Your plan renews on the 26th and will be charged to your card.
Every line begins somewhere new, so the eye has to hunt for the start of each one, which is why this feels slower to read than it should.
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- Body text must reach 4.5:1. Text at 18.66px regular or 14px bold counts as "large" and needs only 3:1 — but do not design to the exception.
- --ds-fg-muted at 12px is the tightest pair in the system. It is verified at 4.6:1 on --ds-surface in both themes; do not use it on --ds-surface-inset without rechecking.
- Never rely on font weight alone to create contrast. A 400-weight and a 600-weight of the same colour measure identically.
Keyboard
| ⌘ / Ctrl + | Browser zoom. The layout must survive 200% without horizontal scrolling. |
| Tab | Headings are not focusable, but heading order determines screen-reader navigation order. |
Screen readers
- Do not skip heading levels. An h2 followed by an h4 tells a screen-reader user that they missed a section.
- Visual size is not semantic level. Styling a <p> to look like a heading gives sighted users a heading and everyone else nothing.
- Avoid text in images. If you must, the alt text carries the full string verbatim.
Focus & touch
- Text itself is not focusable, but links inside prose are. Keep the underline: colour alone fails WCAG 1.4.1 for inline links inside body text.
- Text must reflow at 320px CSS width and at 200% zoom without horizontal scrolling. Setting a min-width in px on a text container is the usual cause of failure.
| Attribute | Applied to | Notes |
|---|---|---|
| <h1> … <h6> | Headings | Use real heading elements. Screen-reader users navigate by heading more than by any other method. |
| aria-level | role="heading" | Only when a real heading element is impossible. It almost always is possible. |
| lang | <html> and inline spans | Drives hyphenation, quotation marks and pronunciation. A French phrase inside English copy needs its own lang. |
Example usage
1// Every string uses a named style. No arbitrary sizes.2<article className="flex flex-col gap-2">3 <span className="text-overline uppercase text-accent-text">Billing</span>4 <h2 className="text-h2 text-fg">Invoice 2026-0431</h2>5 <p className="max-w-[68ch] text-body text-fg-secondary">6 Issued 12 July, due 26 July. Payment is collected automatically.7 </p>8 <p className="text-caption text-fg-muted">Updated 4 minutes ago</p>9</article>1011// Text you click is text-ui (15px), not text-caption. Same at every width.12<nav className="flex flex-col gap-px">13 <a className="flex h-8 items-center rounded-md px-2.5 text-ui">Deployments</a>14 <a className="flex h-8 items-center rounded-md px-2.5 text-ui">Monitoring</a>15</nav>1617// Fields are text-body (16px) — read back, checked, and below 16px iOS18// zooms the page the moment one takes focus.19<input className="h-9 w-full rounded-md px-3 text-body" />2021// Numbers that change or stack must be tabular22<span className="tabular-nums">{formatCurrency(total)}</span>2324// Truncation: one line25<span className="block truncate">{project.name}</span>2627// Truncation: multiple lines28<p className="line-clamp-3">{description}</p>2930// Balance short headings so the last line is not an orphan31<h1 className="text-balance-ds">{title}</h1>CSS
The base layer. Feature settings matter more than most teams realise.
body {
font-family: var(--font-sans);
/* rem, so the browser's own text-size setting still means something.
A px value here overrides the reader and fails WCAG 1.4.4. */
font-size: 1rem; /* 16px at the default root */
line-height: 1.6; /* unitless: survives inheritance and zoom */
/* Inter stylistic sets: disambiguated l/1/I, single-storey a in caps,
and a rounder zero. Materially improves scanning in dense UI. */
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11', 'ss03';
font-variant-ligatures: contextual;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Numbers in any comparable context */
table, code, kbd, pre, [data-tabular] {
font-variant-numeric: tabular-nums;
}
/* Never set a px line-height on body text — it breaks at user zoom.
ch for the measure, because a measure is counted in characters. */
.prose p { line-height: 1.6; max-inline-size: 68ch; }
/* Headings tighten as they grow. Tracking in em: a proportion of the
letterform, so one value is right at 19px and at 44px. */
.h1 { font-size: 2rem; line-height: 1.16; letter-spacing: -0.022em; font-weight: 620; }
.h2 { font-size: 1.5rem; line-height: 1.25; letter-spacing: -0.017em; font-weight: 600; }
.h3 { font-size: 1.1875rem; line-height: 1.35; letter-spacing: -0.012em; font-weight: 600; }
/* Text you click. 15px at every width — a phone gets the same size a
desktop does, because it is held further away, not nearer. */
.ui { font-size: 0.9375rem; line-height: 1.4; font-weight: 470; }
/* clamp() is for display type ONLY. The floor is the size the heading is
allowed to reach on a phone, and it is still bigger than body. */
.display {
font-size: clamp(1.75rem, 1.25rem + 2.2vw, 2.75rem);
line-height: 1.1;
letter-spacing: -0.026em;
}
/* Nothing readable below 12px, and 12px is metadata only. */
.caption { font-size: 0.75rem; line-height: 1.45; }Professional tips
- Inter’s cv02/cv03/cv04/cv11 stylistic sets disambiguate l, 1, I and the single-storey a. In a product full of IDs and tokens this is a real usability gain, not a flourish.
- text-wrap: balance on headings prevents a single-word last line. Use it on headings only — it is O(n²) on long paragraphs and browsers cap it around six lines anyway.
- When a label and its value sit on one row, align them on the baseline rather than the box. Different font sizes have different box heights but share a baseline.
- If a heading needs to be smaller to fit, the container is too narrow or the heading is too long. Shrinking the type is the last resort, not the first.
Performance
- Ship one variable font per family. Four static weights of Inter is roughly 340 kB; the variable file is 48 kB for the Latin subset and covers every weight.
- Self-host with font-display: swap. A blocking webfont request from a third-party CDN is a guaranteed layout shift and a privacy liability.
- Subset aggressively. Latin plus Latin-Extended covers the overwhelming majority of Western products; Cyrillic and Greek subsets load only when the browser needs them.
- Preload only the one font file used above the fold. Preloading all of them competes with your JavaScript for bandwidth.
Common mistakes
- Setting line-height in px. It stops scaling with the font at user zoom, which is a direct WCAG 1.4.4 failure.
- Setting font-size in px. It ignores the reader’s browser text-size setting entirely — the setting they changed precisely because the default was too small for them.
- Shrinking body or navigation text at a mobile breakpoint. The layout is what should respond; the type is what should not.
- Using em for font sizes. It compounds through nesting, so a list inside a list inside an aside arrives at a size nobody chose.
- Using a <p> styled to look like a heading. Sighted users get a heading; screen-reader users get a paragraph and lose the document outline.
- Letting a table cell wrap onto three lines because no max-width was set. A table is scanned in columns, and ragged row heights destroy that.
- Applying letter-spacing to small text to "make it airier". Below 13px, added tracking reduces legibility — the letters stop forming words.
Real-world recommendations
- Ask a colleague to read a screen from two metres away. Whatever they read first is your actual hierarchy, regardless of what you intended.
- Set your browser’s default text size to 20px and reload the product. Everything sized in rem grows with it; everything in px stays exactly where it was, and that difference is the audit.
- Take the phone outside. Sizes that were fine on a calibrated monitor at arm’s length in a dim room are the first thing to fail in daylight on a moving train.
- Set the measure before you set the type size. Once the column width is right, the size that fits comfortably inside it is usually obvious.
- For dense enterprise tables, 13px with 1.6 line height is the practical floor for the cell text. Below that, error rates in data entry rise noticeably — and the header row is a label, not a licence to go to 10px.
- Localisation adds roughly 30% length going from English to German. Design headings and buttons to survive that without re-wrapping the layout.
Four sizes, a floor, four units, and one rule about screens. Everything above is the argument; this is the part to copy into a review.
| Size | Token | What it is for |
|---|---|---|
16px1rem | text-body | Body copy, and every input Prose, descriptions, and any field the user types into or reads a value back from. |
15px0.9375rem | text-ui | Navigation and UI text Sidebar rows, menu items, list rows, tabs — text that is clicked rather than read. |
13px0.8125rem | text-body-sm | Supporting text Help text, table cells, and anything explaining something already on the screen. |
12px0.75rem | text-caption | Captions and metadata only Timestamps, counts, hints. The floor of the scale — nothing readable is set smaller. |