Codeskill

Learn to code, step by step

Typography systems for long-form and UI

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 px locks 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.

PreviousColour – relative colour, contrast, gamut