Typography systems for UI and long-form content – type scales, measure, rhythm, variable fonts, and OpenType features. Good typography is mostly spacing and restraint, not exotic display faces.
Modular scales with clamp()
Define a small set of fluid sizes from one ratio. UI and prose share the same tokens:
:root {
--font-sans: "Inter", system-ui, sans-serif;
--font-serif: "Source Serif 4", Georgia, serif;
--ratio: 1.25;
--text-sm: clamp(0.8125rem, 0.78rem + 0.15vw, 0.875rem);
--text-base: clamp(0.9375rem, 0.88rem + 0.25vw, 1rem);
--text-lg: clamp(1.0625rem, 0.98rem + 0.4vw, 1.25rem);
--text-xl: clamp(1.25rem, 1.1rem + 0.65vw, 1.563rem);
--text-2xl: clamp(1.563rem, 1.3rem + 1.1vw, 1.953rem);
}
Limit the scale to six or seven steps. More becomes noise. Components reference tokens, not raw rem values.
UI vs prose contexts
:where(.ui) {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: 1.5;
letter-spacing: -0.01em;
}
:where(.prose) {
font-family: var(--font-serif);
font-size: var(--text-lg);
line-height: 1.65;
max-width: 65ch;
}
.prose > * + * {
margin-block-start: 1em;
}
UI type stays compact and sans-serif. Long-form prose gets serif (optional), wider line-height, and a measure cap around 65 characters. Mixing both on one page is normal – a dashboard with help articles, for example.
Vertical rhythm
:root {
--leading-ui: 1.5;
--leading-prose: 1.65;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
}
.prose h2 {
font-size: var(--text-xl);
line-height: 1.25;
margin-block: var(--space-4) var(--space-2);
}
.prose h2 + p {
margin-block-start: 0;
}
Heading margins follow a predictable pattern. Collapse extra gap after headings so the first paragraph sits tight underneath.
Variable fonts
@font-face {
font-family: "Inter";
src: url("Inter.var.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap;
}
.display {
font-variation-settings: "wght" 750;
letter-spacing: -0.02em;
}
One file covers many weights. Use axis tweaks sparingly – a slightly heavier display weight, not constant animation on weight axes.
OpenType features
.prose {
font-feature-settings: "kern" 1, "liga" 1, "onum" 1;
}
.tabular-nums {
font-variant-numeric: tabular-nums lining-nums;
}
.code {
font-family: ui-monospace, monospace;
font-size: 0.9em;
font-feature-settings: "liga" 0;
}
Old-style numerals in prose, tabular lining figures in data tables, ligatures off in code. Small details that add up across a product.
Accessibility
- Never below 16px equivalent for body text on mobile unless the UI is genuinely dense (and even then, reconsider).
- Respect user zoom – avoid
pxlocks on text containers. - Line length over 80 characters fatigues readers; under 45 feels cramped for prose.
The next tutorial covers design token pipelines – from Figma variables to CSS custom properties, and keeping design and code in sync.

