TYPE SCALE

Gabrielle – Frontend developer

What is a type scale (and why does it break)?

A type scale is a set of predefined font sizes used across a product. It breaks when sizes are chosen ad-hoc, the ratio changes between breakpoints, or line-height/spacing don’t adapt to viewport changes.

Goal

Build a small, fluid scale that:

  • uses 6–8 steps (not 20),
  • flows smoothly with clamp(),
  • keeps readable line-height and spacing,
  • has predictable, semantic names.

1) Pick a base and ratio

Start with a 16px base (body). Choose a subtle ratio: 1.20 (minor third) for content-heavy pages, or 1.25 for a bit more contrast.

--step--2: clamp(0.78rem, 0.74rem + 0.2vw, 0.88rem); /* xs */
--step--1: clamp(0.88rem, 0.84rem + 0.3vw, 0.95rem); /* sm */
--step-0 : 1rem;                                      /* body */
--step-1 : clamp(1.1rem, 1.03rem + 0.5vw, 1.25rem);   /* h6 */
--step-2 : clamp(1.35rem, 1.2rem  + 1vw,  1.56rem);   /* h5 */
--step-3 : clamp(1.62rem, 1.35rem + 1.8vw, 1.95rem);  /* h4 */
--step-4 : clamp(1.95rem, 1.6rem  + 2.6vw, 2.44rem);  /* h3 */
--step-5 : clamp(2.44rem, 2rem   + 3.6vw, 3.05rem);   /* h2 */
--step-6 : clamp(3.05rem, 2.4rem + 5vw,  3.81rem);    /* h1 */

Rule of thumb: small steps grow less, large steps grow more.

2) Implement with CSS variables

Define steps once, then map them to elements. Use clamp(min, preferred, max) where the middle term is a small linear function of viewport width.

:root{
  /* Scale (adjust coefficients to your layout width) */
  --step--2: clamp(0.78rem, 0.74rem + 0.20vw, 0.88rem);
  --step--1: clamp(0.88rem, 0.84rem + 0.30vw, 0.95rem);
  --step-0 : 1rem;
  --step-1 : clamp(1.10rem, 1.03rem + 0.50vw, 1.25rem);
  --step-2 : clamp(1.35rem, 1.20rem + 1.00vw, 1.56rem);
  --step-3 : clamp(1.62rem, 1.35rem + 1.80vw, 1.95rem);
  --step-4 : clamp(1.95rem, 1.60rem + 2.60vw, 2.44rem);
  --step-5 : clamp(2.44rem, 2.00rem + 3.60vw, 3.05rem);
  --step-6 : clamp(3.05rem, 2.40rem + 5.00vw, 3.81rem);

  /* Rhythm tokens */
  --lh-tight: 1.15;
  --lh-normal: 1.45;
  --lh-loose: 1.60;

  --space-1: 0.25rem;
  --space-2: 0.50rem;
  --space-3: 0.75rem;
  --space-4: 1.00rem;
  --space-6: 1.50rem;
  --space-8: 2.00rem;
}

/* Map steps to elements */
body{ font-size: var(--step-0); line-height: var(--lh-loose); }
h1{ font-size: var(--step-6); line-height: var(--lh-tight); margin: 0 0 var(--space-6); }
h2{ font-size: var(--step-5); line-height: var(--lh-tight); margin: 0 0 var(--space-4); }
h3{ font-size: var(--step-4); line-height: 1.25;           margin: 0 0 var(--space-3); }
h4{ font-size: var(--step-3); line-height: 1.25;           margin: 0 0 var(--space-3); }

p, ul, ol{ margin: 0 0 var(--space-4); }
small, .caption{ font-size: var(--step--1); line-height: var(--lh-normal); }

3) Fluid without breakpoints

clamp() avoids hard jumps. If you still want a manual breakpoint (for very large/small screens), tweak a step inside media queries:

@media (min-width: 1280px){
  :root{ --step-6: clamp(3.2rem, 2.2rem + 3vw, 4.2rem); }
}

4) Line-height & spacing rules

  • Display sizes (H1–H2): 1.15–1.25
  • Text sizes (body): 1.45–1.60
  • Keep vertical rhythm via spacing tokens (e.g., paragraph margin = --space-4).

5) Naming: semantic, not numeric

Prefer semantic tokens for reuse:

:root{
  --fs-body: var(--step-0);
  --fs-lead: var(--step-2);
  --fs-kicker: var(--step--1);
  --fs-h1: var(--step-6);
  --fs-h2: var(--step-5);
  --fs-h3: var(--step-4);
}
.lead{ font-size: var(--fs-lead); line-height: var(--lh-normal); }
.kicker{ font-size: var(--fs-kicker); letter-spacing: .04em; text-transform: uppercase; }

6) Accessibility checks

  • Min body size: don’t go below 16px; 17–18px is fine for text-heavy pages.
  • Contrast: test headings vs background (WCAG AA at minimum).
  • Respect user settings: use rem and don’t lock zoom.

7) Quick starter snippet

Copy this and tweak only the middle term in clamp() until it feels right for your container width:

:root{
  --step-0: 1rem;
  --step-2: clamp(1.35rem, 1.10rem + 1vw, 1.56rem);
  --step-4: clamp(1.95rem, 1.40rem + 2.6vw, 2.44rem);
  --step-6: clamp(3.05rem, 2.00rem + 5vw, 3.81rem);
}
h1{ font-size: var(--step-6); line-height: 1.20; }
h2{ font-size: var(--step-4); line-height: 1.25; }
.lead{ font-size: var(--step-2); line-height: 1.45; }

Common mistakes

Mixing too many ratios (e.g., 1.25 on mobile, 1.5 on desktop) → visible jumps. Ignoring line-height/spacing when sizes change → cramped or airy blocks. Basing growth on the full viewport while content sits in a narrow column → sizes grow too fast. Use small coefficients (e.g., + 0.5vw to + 3vw).

Takeaway

Define 6–8 steps, make them fluid with clamp(), and lock rhythm with line-height + spacing tokens. Your headings will scale smoothly, remain readable, and won’t “snap” across breakpoints.