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
remand 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
+ 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.