Typography, fonts and fluid type
Font stacks and web fonts, line-height and measure, text wrapping, and type that scales with clamp instead of breakpoints.
Stacks, web fonts and loading
Typography is most of what people read on a page, and most of its cost is bytes. A system font stack costs nothing and renders instantly; a custom web font costs a request and a moment of layout risk.
:root {
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: ui-monospace, "SFMono-Regular", Consolas, monospace;
}
body { font-family: var(--font-sans); }
@font-face {
font-family: "Brand";
src: url("/fonts/brand.woff2") format("woff2");
font-display: swap;
unicode-range: U+0000-00FF;
}- Ship WOFF2 only — every browser that matters supports it, so older formats are dead weight.
font-display: swapshows fallback text immediately and swaps when the font arrives, instead of leaving the page blank.unicode-rangesplits a font into subsets so a Latin-only page never downloads Cyrillic glyphs.- Preload the one font used above the fold with
<link rel="preload" as="font" crossorigin>; preloading everything competes with the critical CSS. - Always end a stack with a generic family so the browser has a guaranteed fallback.
Line-height, measure and wrapping
Readable text depends on three numbers rather than the typeface itself: the size contrast between levels, the space between lines, and the length of a line of text.
.prose {
font-size: 1.125rem; /* 18px when the root is 16px */
line-height: 1.6; /* unitless: multiplied by each element's own size */
max-inline-size: 68ch; /* the measure: roughly 60 to 75 characters */
}
h2 { text-wrap: balance; }
p { text-wrap: pretty; }
td { font-variant-numeric: tabular-nums; }| Property | Effect | Watch out for |
|---|---|---|
line-height unitless | Scales with each element's font size | A unit such as px freezes it and breaks inheritance |
max-inline-size | Caps the line length | Use ch, not px, so it tracks the font |
text-wrap: balance | Evens out heading lines | Browser-limited to a handful of lines — headings only |
text-wrap: pretty | Avoids single-word last lines in prose | A refinement, never a requirement |
tabular-nums | Fixed-width digits | Only for tables and counters, not body text |
letter-spacing | Tightens large text, loosens small caps | Never letter-space lowercase body copy |
Fluid type with clamp
clamp(min, preferred, max) interpolates between two sizes across a range of viewport widths, which removes a whole class of breakpoints. The preferred value is normally a rem base plus a vw slope.
h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }
.lede { font-size: clamp(1rem, 0.95rem + 0.4vw, 1.25rem); }
h2 { font-size: clamp(1.35rem, 1.1rem + 1.1vw, 1.9rem); }Keep the middle term in rem plus vw rather than pure vw: a pure viewport unit ignores the user's own font-size preference, which is exactly the accessibility setting fluid type is supposed to respect.
- Build a small scale — a 1.2 or 1.25 ratio between steps is plenty for most sites.
- Clamp heading sizes aggressively and body sizes gently; body text should barely move.
- Check 320px and a very wide monitor: those two extremes are where a bad clamp shows up.
- Pair fluid type with fluid spacing so the rhythm does not become lopsided at small sizes.
user-scalable=no or a maximum-scale of 1 in the viewport meta tag. Fluid type reduces how often people need to zoom; it is not a substitute for zoom.FAQ
Should font sizes be in rem or px?
rem for anything the reader can zoom or that should respect a changed browser default, which means all type and most spacing. Reserve px for hairlines such as 1px borders.Why does my web font flash or jump?
font-display: swap makes the flash deliberate rather than invisible, and size-adjusted fallback metrics reduce the jump.Related
Units, custom properties and calc Color, backgrounds and dark mode
Last refreshed 2026-09-18.