TYPOGRAPHY

Gabriele – Frontend developer

Why typography matters

Typography sets the voice of a product. Good type makes content clear, scannable, and trustworthy. Bad type confuses users, creates visual noise, and hurts accessibility.

Font choices

  • System stack — fastest, no downloads: font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  • Variable fonts — one file, multiple weights and optical sizes. Prefer ranges like wght 300–800, and use opsz if available.
  • Always check language support (diacritics, ligatures) and license terms for web embedding.

Scale and hierarchy

Use 6–8 steps, not 20. Keep them fluid with clamp(). Base: 16px, ratio ~1.2 for content-heavy apps.

:root{
  --fs-xs: clamp(.78rem, .74rem + .20vw, .88rem);
  --fs-sm: clamp(.88rem, .84rem + .30vw, .95rem);
  --fs-base: 1rem;
  --fs-h6: clamp(1.1rem,1.03rem + .5vw,1.25rem);
  --fs-h5: clamp(1.35rem,1.2rem + 1vw,1.56rem);
  --fs-h4: clamp(1.62rem,1.35rem + 1.8vw,1.95rem);
  --fs-h3: clamp(1.95rem,1.6rem + 2.6vw,2.44rem);
  --fs-h2: clamp(2.44rem,2rem + 3.6vw,3.05rem);
  --fs-h1: clamp(3.05rem,2.4rem + 5vw,3.81rem);
}
body{ font-size: var(--fs-base); }
h1{ font-size: var(--fs-h1); }
h2{ font-size: var(--fs-h2); }
h3{ font-size: var(--fs-h3); }

Line-height & vertical rhythm

  • Body text: 1.45–1.6
  • Headings: 1.15–1.25
  • Use spacing tokens instead of arbitrary margins.
:root{
  --lh-tight: 1.18; --lh-text: 1.55;
  --space-2: .5rem; --space-4: 1rem; --space-6: 1.5rem;
}
p{ line-height: var(--lh-text); margin: 0 0 var(--space-4); }
h1,h2,h3{ line-height: var(--lh-tight); margin: 0 0 var(--space-6); }

Measure (line length)

Optimal readability: 60–75 characters per line. On small screens, width shrinks naturally.

.prose{ max-width: 72ch; }
.prose img, .prose pre{ max-width: 100%; }

Contrast & accessibility

  • Minimum contrast: WCAG AA (4.5:1 for body, 3:1 for headings).
  • Avoid text on noisy images/gradients unless you add an overlay (“scrim”).
  • Use rem units, respect user zoom.

Tracking & ligatures

  • Large display headings: slight negative tracking (-0.01em).
  • Labels or all-caps: positive tracking (.02–.04em).
  • Code blocks: disable fancy ligatures if distracting.
h1{ letter-spacing: -.012em; }
.kicker{ text-transform: uppercase; letter-spacing: .04em; font-size: var(--fs-sm); }
code{ font-variant-ligatures: none; }

Font loading (performance)

  • Preload critical weights, use font-display: swap.
  • Override font metrics to avoid CLS jumps.
@font-face{
  font-family: 'YourVF';
  src: url('/fonts/YourVF.woff2') format('woff2-variations');
  font-weight: 300 800;
  font-display: swap;
  ascent-override: 90%; descent-override: 22%; line-gap-override: 0%;
}

Semantic tokens

Instead of “h2 = 2rem”, define semantic roles. Easier to theme and adjust later.

:root{
  --fs-body: var(--fs-base);
  --fs-lead: var(--fs-h5);
  --fs-kicker: var(--fs-sm);
  --fs-quote: var(--fs-h4);
}
.lead{ font-size: var(--fs-lead); line-height: 1.45; }
blockquote{ font-size: var(--fs-quote); line-height: 1.35; }

Quick checklist

6–8 step fluid scale with clamp() Base text 16–18px, body LH 1.45–1.6 Measure: 60–75ch Contrast ≥ WCAG AA Preload fonts + font-display: swap + metrics override Semantic tokens: body, lead, kicker, caption, code