Skip to content

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.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Type scale
SpecimenTokenSize / LH / Tracking
The quick brown foxtext-display44px / 1.08 / -0.028em
The quick brown foxtext-h132px / 1.16 / -0.022em
The quick brown foxtext-h224px / 1.25 / -0.017em
The quick brown foxtext-h319px / 1.35 / -0.012em
The quick brown foxtext-h416px / 1.45 / -0.006em
The quick brown foxtext-body-lg17px / 1.62 / -0.004em
The quick brown foxtext-body16px / 1.6 / -0.002em
The quick brown foxtext-ui15px / 1.4 / -0.001em
The quick brown foxtext-body-sm13px / 1.6 / 0
The quick brown foxtext-label13px / 1.2 / 0.001em
The quick brown foxtext-label-sm12px / 1.2 / 0.004em
The quick brown foxtext-caption12px / 1.45 / 0.002em
Section labeltext-overline12px / 1.2 / 0.09em
The quick brown foxtext-code13px / 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.

110 charactersThe eye loses its place on the return sweep.

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.

68 charactersComfortable for sustained reading.

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.

28 charactersToo many returns; rhythm breaks up.

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.

UnitUse it forExample
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().

Desktop1440px wide
DeploymentsBody copy stays 16px.Updated 4 minutes ago
Tablet834px wide
DeploymentsBody copy stays 16px.Updated 4 minutes ago
Phone390px wide
DeploymentsBody copy stays 16px.Updated 4 minutes ago
What does changeLayout, measure and the display sizes — never the reading sizes.
A display heading scales

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.

Hierarchy by size + weight + colour
Billing

Invoice 2026-0431

Issued 12 July · Due 26 July

Auto-charged to Visa ···4242

Hierarchy by size alone
BILLINGInvoice 2026-0431Issued 12 July · Due 26 JulyAuto-charged to Visa ···4242

Numerals

Tabular figures are mandatory anywhere a number can change or be compared vertically. Proportional figures are for prose.

Tabular — table, metric, timer
1,428.009,911.15111.11
Proportional — prose only
1,428.009,911.15111.11

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.

Heading text
Primary--ds-fg
Body copy
Secondary--ds-fg-secondary
Caption
Muted--ds-fg-muted
Unavailable
Disabled--ds-fg-disabled
useId()
Code
A very long project name
TruncatedOne line, ellipsis
selected text
Selection

Anatomy

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--ds-fg—Headings
--ds-fg-secondary—Body
--ds-fg-muted—Captions and metadata

Spacing

TokenValueUsed for
heading → bodyBinding a heading to its paragraph
section gapSeparating major sections

Typography

TokenValueUsed for
--font-sansAll interface text
--font-monoCode, tokens, IDs, tabular data
--text-displayMarketing hero
--text-h1Page title
--text-h2Section heading
--text-h3Subsection, dialog title
--text-h4Card title
--text-body-lgLead paragraph, long-form
--text-bodyBody copy and every input
--text-uiNavigation, menus, list rows
--text-body-smSupporting copy, table cells
--text-labelButtons, form labels, tabs
--text-captionMetadata — the floor
--text-overlineEyebrows, uppercase only

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightTypeMax widthWhen to use
Display44px620 weight18chMarketing hero only. Never inside the product UI.
H132px620 weight28chOne per screen, in the page header.
H224px600 weight40chMajor sections within a page.
H319px600 weight48chSubsections and dialog titles.
H416px600 weight56chCard titles and group headings.
Body large17px400 weight68chDocumentation and long-form reading.
Body16px400 weight68chProse, and every field the user types into.
UI15px470 weight40chNavigation, menus, list rows. Never shrinks on a phone.
Body small13px400 weight72chSupporting copy: help text, table cells, dense panels.
Caption12px400 weight60chTimestamps, counts, hints. The floor — nothing readable goes under it.

Do

font-size: 1rem; line-height: 1.6;font-size: 16px; line-height: 26px;
Set sizes in rem and leading unitlessrem answers the browser’s own text-size setting, which a px value silently ignores — a WCAG 1.4.4 failure that no amount of testing at 100% zoom will ever surface. A unitless line height multiplies against whatever size the element inherits, so one value stays correct everywhere.
Desktop 15pxPhone 15pxsame
Hold the size on small screensA phone is held further from the eye than a laptop, in worse light, more often one-handed. Shrinking the type there inverts the need. Responsive layout is the lever: narrow the measure, drop a column, show less — the sizes stay put.

Sixty-eight characters is roughly eleven words per line, which is close to the span the eye can track without a conscious return sweep.

Cap the measure in ch, not pxA max-width of 68ch stays correct if the font size changes, if the user zooms, or if a locale swaps to a wider script. A pixel width silently becomes the wrong measure.

Section heading

Its paragraph, close beneath.

Bind headings to their content with proximityGestalt proximity: the gap above a heading must be visibly larger than the gap below it. Equal gaps make the heading float between two blocks and belong to neither.
$ 1,428.00$ 9,911.15$ 111.11
Use tabular figures for anything comparableProportional digits have different widths, so a column of numbers will not align on the decimal and the digits jitter as values update. font-variant-numeric: tabular-nums fixes both.
600620700
Set an explicit weight on every heading styleVariable fonts interpolate. Relying on a browser default gives you 700 in some contexts and 400 in others, and the difference is visible when two headings sit side by side.

Don't

font-size: clamp(0.8125rem, 2vw, 0.9375rem);font-size: clamp(1.75rem, 1.25rem + 2.2vw, 2.75rem); /* display only */
Do not clamp() ordinary UI textclamp() is for display sizes, where a 44px hero genuinely cannot stay 44px at 390px. Applied to body or navigation it produces text that is 15px on the machine you designed on and 13px on the device most people read it on — a shrink nobody asked for, in the one place it hurts.
10px badge11px legend12px floorfirst two: no
Do not set anything readable below 12pxTwelve is the floor, and it is reserved for metadata — timestamps, counts, hints. An 11px legend or a 10px badge is text the product needs the reader to have, set at a size a great many adults cannot resolve at arm’s length — near focus starts changing in the mid-forties, and it does not change back.

Your subscription will renew automatically on the 26th unless cancelled beforehand

Do not set long text in uppercaseUppercase removes ascenders and descenders, which is what the eye uses to recognise word shapes. Reading speed drops measurably past about three words.

Your plan renews on the 26th and will be charged to your card.

Do not use more than two weights in one blockWeight is a hierarchy signal. Three weights in a paragraph means three levels of importance in a place where there should be one, and the eye stops trusting the signal.

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.

Do not centre body copyCentred text gives every line a different starting x-position, so the return sweep has to search for the start of each line. Acceptable for one or two lines; punishing for four.
1717.51819pick one
Do not invent a size between stepsA 17.5px heading is not meaningfully different from 17px, but it is one more value in the codebase, one more thing to be inconsistent about, and it will be copied.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.4.4Resize TextAA1.4.8Visual PresentationAAA1.4.12Text SpacingAA2.4.6Headings and LabelsAA1.3.1Info and RelationshipsA

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.
TabHeadings 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.
AttributeApplied toNotes
<h1> … <h6>HeadingsUse real heading elements. Screen-reader users navigate by heading more than by any other method.
aria-levelrole="heading"Only when a real heading element is impossible. It almost always is possible.
lang<html> and inline spansDrives hyphenation, quotation marks and pronunciation. A French phrase inside English copy needs its own lang.

Code

Example usage

tsx
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.

css
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; }

Notes

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.

The size standard

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.

SizeTokenWhat it is for
16px1remtext-body

Body copy, and every input

Prose, descriptions, and any field the user types into or reads a value back from.

15px0.9375remtext-ui

Navigation and UI text

Sidebar rows, menu items, list rows, tabs — text that is clicked rather than read.

13px0.8125remtext-body-sm

Supporting text

Help text, table cells, and anything explaining something already on the screen.

12px0.75remtext-caption

Captions and metadata only

Timestamps, counts, hints. The floor of the scale — nothing readable is set smaller.