Episode 1 — Fundamentals / 1.9 — CSS Responsive Design

1.9.e — Fluid Typography

In one sentence: Fluid typography uses clamp() to scale text smoothly between a minimum and maximum size as the viewport changes — eliminating jarring jumps at breakpoints and keeping readability locked in at every width.

Navigation: ← 1.9.d — Responsive Images · 1.9.f — Spacing & Rhythm →


1. The problem with fixed font sizes

Stepped approach (media queries only)

h1 { font-size: 1.5rem; }

@media (min-width: 768px)  { h1 { font-size: 2rem; } }
@media (min-width: 1200px) { h1 { font-size: 3rem; } }

This creates jumps — at 767px the heading is 1.5rem, at 768px it snaps to 2rem. Between breakpoints the size is frozen, even though the viewport keeps changing.

Viewport-only approach (dangerous)

h1 { font-size: 5vw; }

Text scales continuously but has no bounds — at 320px it's 16px (barely readable), at 1920px it's 96px (absurdly large). Users who zoom the browser get no benefit because vw doesn't respond to zoom.


2. clamp() to the rescue

h1 {
  font-size: clamp(1.5rem, 2.5vw + 0.5rem, 3rem);
}

How clamp(min, preferred, max) works

ParameterRoleExample
minFloor — never go below this1.5rem (24px at default)
preferredScales with viewport2.5vw + 0.5rem
maxCeiling — never go above this3rem (48px at default)
Font size
  3rem ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  ← max
          ╱‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
         ╱  preferred value scales linearly
        ╱
1.5rem ─  ← min
       ├────────────┼────────────┼────────────┤
      320px       768px       1200px       1920px
                     Viewport width

The value is clamped: below a certain viewport the min kicks in, above a certain viewport the max kicks in, and in between the preferred value slides smoothly.


3. Calculating the preferred value

The preferred value typically combines a viewport unit with a rem offset so that:

  • The viewport unit provides fluid scaling.
  • The rem offset ensures a reasonable base and responds to user zoom.

Manual formula

To scale from min-size at min-viewport to max-size at max-viewport:

preferred = viewport-slope × 100vw + rem-offset

where:
  viewport-slope = (max-size - min-size) / (max-viewport - min-viewport)
  rem-offset     = min-size - (viewport-slope × min-viewport)

Example: Scale from 1.5rem (24px) at 320px to 3rem (48px) at 1200px:

slope  = (48 - 24) / (1200 - 320) = 24 / 880 ≈ 0.02727
offset = 24 - (0.02727 × 320) = 24 - 8.727 = 15.273px ≈ 0.955rem

→ clamp(1.5rem, 2.727vw + 0.955rem, 3rem)

Don't memorize this — use a tool (see section 7).


4. Fluid type scale

A type scale is a set of harmonious sizes for body, headings, and small text. A fluid type scale makes every step scale smoothly.

:root {
  --text-sm:   clamp(0.8rem,   0.17vw + 0.75rem,  0.875rem);
  --text-base: clamp(1rem,     0.34vw + 0.91rem,   1.125rem);
  --text-lg:   clamp(1.25rem,  0.56vw + 1.08rem,   1.5rem);
  --text-xl:   clamp(1.563rem, 0.89vw + 1.3rem,    2rem);
  --text-2xl:  clamp(1.953rem, 1.36vw + 1.56rem,   2.75rem);
  --text-3xl:  clamp(2.441rem, 2.01vw + 1.87rem,   3.5rem);
}

body      { font-size: var(--text-base); }
h1        { font-size: var(--text-3xl); }
h2        { font-size: var(--text-2xl); }
h3        { font-size: var(--text-xl); }
.caption  { font-size: var(--text-sm); }

5. Line length and readability

Fluid font sizes affect line length (ch or character count per line). Optimal reading is 45–75 characters per line.

.prose {
  font-size: var(--text-base);
  max-width: 65ch;
  line-height: 1.6;
}

As font size scales up, ch units grow proportionally, keeping line length comfortable without extra breakpoints.


6. Accessibility: respecting user preferences

The rem boundary rule

Always set clamp() bounds in rem, not px. When a user sets their browser's base font size to 20px (for low vision), rem-based bounds scale up accordingly.

/* Good — respects user zoom */
h1 { font-size: clamp(1.5rem, 2.5vw + 0.5rem, 3rem); }

/* Bad — ignores user zoom for bounds */
h1 { font-size: clamp(24px, 2.5vw + 8px, 48px); }

WCAG 1.4.4 — Resize text

Content must be resizable to 200% without loss of functionality. clamp() with rem bounds passes this test because the bounds themselves scale with the user's zoom level.

Test it

  1. Open browser settings → set font size to "Very Large."
  2. Use Ctrl+ / Cmd+ to zoom to 200%.
  3. Verify all text scales and nothing overflows or becomes unreadable.

7. Tools for generating fluid type scales

ToolURLWhat it does
Utopiautopia.fyiGenerates fluid type + spacing scales with clamp()
Fluid Type Scale Calculatorfluid-type-scale.comVisual slider for min/max size and viewport
Modern Fluid Typographymodern-fluid-typography.vercel.appInteractive calculator with live preview
Type Scaletypescale.comTraditional type scale (not fluid, but good for ratios)

Utopia workflow:

  1. Set min viewport (e.g., 320px) and max viewport (e.g., 1200px).
  2. Choose a scale ratio (e.g., 1.2 minor third → 1.333 perfect fourth).
  3. Set min and max base sizes.
  4. Copy the generated clamp() custom properties into your CSS.

8. Fluid sizing beyond typography

clamp() works for any length property:

/* Fluid padding */
.section {
  padding-block: clamp(2rem, 5vw, 6rem);
}

/* Fluid gap */
.grid {
  gap: clamp(1rem, 2vw, 3rem);
}

/* Fluid max-width */
.container {
  max-width: clamp(320px, 90vw, 1200px);
  margin-inline: auto;
}

9. Key takeaways

  1. clamp(min, preferred, max) creates smooth, bounded scaling — no breakpoint jumps.
  2. The preferred value mixes vw + rem for viewport-responsive scaling that still respects zoom.
  3. Set bounds in rem for accessibility — never px bounds on text.
  4. Build a fluid type scale with tools like Utopia instead of computing values by hand.
  5. Fluid techniques apply to spacing and layout, not just fonts.

Explain-It Challenge

Explain without notes:

  1. Why is font-size: 5vw dangerous without clamp(), and what two problems does it create?
  2. Walk through what happens to clamp(1rem, 2vw + 0.5rem, 2rem) at viewport widths 320px, 800px, and 2000px.
  3. Why must the min and max values in clamp() use rem instead of px for body text?

Navigation: ← 1.9.d — Responsive Images · 1.9.f — Spacing & Rhythm →