Typography

Set text that is comfortable to read, and load a webfont without a flash of nothing.

08of 26
≈ 12Min read
9Parts

The font stack

font-family is a list of preferences. The browser takes the first one it has, so the last entry must be a generic family that always exists: sans-serif, serif, monospace.

two useful stacks
/* the system font: no download at all, and it looks native everywhere */
body {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* a webfont with a real fallback behind it */
h1, h2 {
  font-family: "Space Grotesk", system-ui, sans-serif;
}

Quote a family name containing spaces. The fallback is not decoration — it is what people see for the first few hundred milliseconds, and what they see forever if the download fails.

Loading a webfont

  • WOFF2 only. Every browser that matters supports it, and it is the smallest.
  • Preload the one or two faces used above the fold: <link rel="preload" as="font" type="font/woff2" crossorigin href="…">.
  • crossorigin is required on font preloads even for your own domain, or the file is fetched twice.
  • One variable font usually replaces four static files and is smaller than two of them.
@font-face
@font-face {
  font-family: "Space Grotesk";
  src: url("/fonts/space-grotesk.woff2") format("woff2");
  font-weight: 400 700;          /* a variable font covers a range */
  font-display: swap;
  font-style: normal;
}
font-displayWhat the reader sees
swapThe fallback immediately, then a swap when the font lands. Nothing is ever invisible.
optionalThe fallback, and the font is used only if it arrives almost at once. No layout shift at all.
fallbackA very short blank period, then the fallback, then a swap within a few seconds.
blockInvisible text for up to three seconds. Almost never what you want.

Size, and the shorthand

font is a shorthand that resets things
/* longhand */
h2 {
  font-size: 1.5rem;
  line-height: 1.2;
  font-weight: 600;
}

/* shorthand: size and family are REQUIRED, and everything
   you leave out is reset to its initial value */
h2 { font: 600 1.5rem/1.2 "Space Grotesk", sans-serif; }

/* which is why this one line is so useful on form controls */
input, button, select, textarea { font: inherit; }

Line height

Give line-height a unitless number. 1.5 means one and a half times this element's own font size, and it is recalculated for every descendant. 24px or 150% computes once on the parent and is then inherited as a fixed value, so a larger heading inside it gets the same cramped leading.

  • Body text: 1.5 to 1.7.
  • Large headings: 1 to 1.2 — big type needs less.
  • Buttons and single-line labels: 1.
  • The bigger the text, the tighter the leading. The longer the line, the looser.

Measure

Line length matters more than font size for reading comfort. Somewhere between 45 and 75 characters is the range; ch expresses it directly.

the one rule that improves most pages
.prose p,
.prose li {
  max-width: 68ch;
}

Weight and style

numbers, not names
font-weight: 400;   /* normal */
font-weight: 700;   /* bold */
font-weight: 350;   /* a variable font can do anything in its range */

font-style: italic;
font-variant-numeric: tabular-nums;   /* digits in columns — essential in tables */
font-feature-settings: "ss01";        /* a stylistic set the font provides */
Faux bold

If the family has no 700 face, the browser synthesises one by smearing the 400. It looks wrong and it is a sign a weight is missing from your @font-face set.

Spacing and wrapping

letter-spacing in em scales with the font size, which is what you want. Never letter-space lower-case body text — it slows reading down.

the details that show
h1 {
  letter-spacing: -0.03em;   /* large type needs tightening */
  text-wrap: balance;        /* even line lengths in a short heading */
}

p {
  text-wrap: pretty;         /* avoids a single word on the last line */
  overflow-wrap: break-word; /* a long URL breaks instead of overflowing */
  hyphens: auto;             /* needs lang to be set correctly */
}

A type scale

Choosing six sizes in advance and using only those is the single biggest difference between a page that looks designed and one that does not.

sizes chosen once
:root {
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: clamp(1.25rem, 1.1rem + 0.7vw, 1.6rem);
  --text-2xl: clamp(1.75rem, 1.4rem + 1.6vw, 2.6rem);
  --text-3xl: clamp(2.5rem, 1.8rem + 3.4vw, 5rem);
}

Truncating

one line, and several
.one-line {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.three-lines {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

Truncated text is still in the DOM, so a screen reader reads all of it. That is usually right — but never truncate something the reader has no other way to see.

Practice