ESC

Type to search the knowledge base.

Fluid Typography with clamp

Use clamp(min, preferred, max) for fluid type and spacing — viewport units, the linear equation pattern, and accessibility floors.

intermediate3 min read
  • css
  • clamp
  • typography

Fluid type scales continuously between a minimum and maximum so you drop a handful of font-size media queries. clamp(MIN, VAL, MAX) is the primitive: never smaller than MIN, never larger than MAX, prefer VAL in between.

Docs: MDN clamp(), CSS values, fluid type techniques.

The pattern

:root {
  --step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --step-1: clamp(1.25rem, 1.1rem + 0.75vw, 1.75rem);
  --step-2: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
}

body {
  font-size: var(--step-0);
  line-height: 1.5;
}

h1 {
  font-size: var(--step-2);
  line-height: 1.15;
}

VAL is often a linear mix of rem + vw so type grows with the viewport but stays anchored to root font size (user zoom / browser defaults still matter).

Why not raw vw alone?

/* Risky */
h1 {
  font-size: 5vw;
}

Pure vw can become unreadably small on narrow screens and huge on ultra-wide ones, and it can ignore user font-size preferences more harshly depending on setup. clamp enforces floors and ceilings; including rem in the middle term respects root sizing better than pure viewport units.

Computing the middle term

You want size A at viewport width Vmin and size B at Vmax. The line:

[ font-size = slope \times 100vw + intercept ]

Many teams use a generator (Utopia, open-type-tools) or a small calc:

/* Example hand-tuned */
h1 {
  font-size: clamp(1.75rem, 1rem + 2.5vw, 3rem);
}

Test at 320px, 768px, 1280px, 1600px — not only the happy laptop width.

Fluid spacing too

.section {
  padding-block: clamp(2rem, 1.5rem + 3vw, 5rem);
  padding-inline: clamp(1rem, 0.5rem + 2vw, 2.5rem);
  gap: clamp(0.75rem, 0.5rem + 1vw, 1.5rem);
}

Keep vertical rhythm related to type steps so headings don’t float in arbitrary space.

Container-relative fluid type

When the component lives in a sidebar, viewport-based type can feel wrong. Prefer container query units:

.card {
  container-type: inline-size;
}

.card__title {
  font-size: clamp(1rem, 2.5cqi + 0.75rem, 1.5rem);
}

Accessibility floors

  • Minimum body text: commonly ≥ 1rem (16px) unless the product has a strong reason and tested zoom.
  • Don’t disable zoom (user-scalable=no).
  • Check line length: fluid type on full-bleed containers can produce 120+ character lines — constrain max-inline-size (e.g. 70ch).
.prose {
  font-size: var(--step-0);
  max-inline-size: 65ch;
}

Interview out-loud

“clamp(min, preferred, max) caps a fluid value. For type I use a rem-based minimum, a rem+vw preferred middle, and a rem maximum so text scales with the viewport without becoming unusable. I also clamp spacing and often prefer container units for component-local type. Media queries still handle font family or major layout shifts.”

Footguns

  1. clamp with incompatible units that don’t compute.
  2. Only testing desktop widths.
  3. Fluid headings without max measure → awkward wraps.
  4. Nested clamps that nobody can reason about — name tokens.
  5. Assuming clamp replaces all typography design — line-height and weight still matter.

Type scale example

:root {
  --fs-sm: clamp(0.875rem, 0.85rem + 0.15vw, 0.95rem);
  --fs-md: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --fs-lg: clamp(1.25rem, 1.1rem + 0.75vw, 1.75rem);
  --fs-xl: clamp(1.75rem, 1.3rem + 1.8vw, 3rem);
  --lh-tight: 1.2;
  --lh-body: 1.55;
}
body { font-size: var(--fs-md); line-height: var(--lh-body); }
h1 { font-size: var(--fs-xl); line-height: var(--lh-tight); }
h2 { font-size: var(--fs-lg); line-height: var(--lh-tight); }
small, .meta { font-size: var(--fs-sm); }

Lock the scale early; fluid type without a scale becomes one-off clamps nobody trusts. Pair with a max measure on prose containers so large desktops do not produce 120-character lines.

Further reading

Related guides