Lesson 20 • Advanced Track
Responsive Typography with clamp()
By the end of this lesson you'll be able to make text that scales smoothly from phone to desktop with a single line of CSS, build a balanced type scale, set a comfortable line length, and do it all without breaking the zoom your low-vision readers rely on.
What You'll Learn
font-size and line-height, and you'll get more out of it if you've met design tokens in CSS Custom Properties. No maths background needed — every formula here is explained in plain English.💡 Think of It Like This
clamp() is a thermostat with a guaranteed range. You set a minimum the room can never fall below, a preferred setting that drifts with the weather outside (your viewport width), and a maximum it can never exceed. The temperature glides between those bounds on its own — you never have to step in for "if it's between 18° and 22°, do this."
Old-school responsive type was the opposite: a stack of if statements (media queries) that snapped the font from one fixed size to the next at hard breakpoints — comfortable at each chosen width, jumpy in between. clamp() replaces the staircase with a ramp. One line gives you a smooth, bounded size at every width.
1. rem vs em vs px — Pick the Right Unit
Before you can size text responsively, you need to size it accessibly, and that comes down to the unit. A px is a fixed device pixel — it never changes. A rem is relative to the root font size (the <html> element), so 1rem follows whatever base size the user picked. An em is relative to the current element's font size, so it tracks its immediate context — and compounds when you nest things.
| Unit | Relative to | Best for | Zoom-safe? |
|---|---|---|---|
rem | Root (html) font size | Font sizes, spacing, layout | ✅ Yes |
em | This element's font size | Padding/gaps that track their text | ✅ Yes (but compounds) |
px | Nothing — fixed | Borders, shadows, hairlines | ⚠️ Ignores font preference |
vw | 1% of viewport width | Fluid growth (inside clamp!) | ❌ Ignores zoom on its own |
Run the worked example and watch the trap spring: the em heading nested inside another em heading gets huge, because each em multiplies the one above it.
Worked example: how each unit responds
rem follows the root, em compounds, px stays put
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Units compared</title>
<style>
/* The root font size. Change 16px to 24px and watch every rem grow,
every px stay frozen — that is exactly what a user's "larger text"
browser setting does. */
html { font-size: 16px; }
body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:24px; }
.row { b
...2. Fluid Type with clamp(min, preferred-vw, max)
clamp() takes three values and returns the middle one — unless it strays outside the bounds, in which case it's pinned to the nearest one. So clamp(2rem, 5vw, 4rem) means: aim for 5vw, but never smaller than 2rem and never larger than 4rem. It is literally max(min, min(preferred, max)), just readable.
The pattern that keeps it accessible: a rem floor, a vw-based middle so it grows with the screen, and a rem ceiling. Because the floor and ceiling are in rem, browser zoom still enlarges the text — the thing a bare vw can't do. Resize the preview to see it glide.
Worked example: a fluid clamp() heading scale
Resize the preview and watch every heading scale smoothly
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fluid type scale</title>
<style>
* { box-sizing: border-box; margin:0; padding:0; }
body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
/* FLUID TYPE SCALE — one clamp() per level, no media queries.
Pattern is always clamp(rem-floor, vw-growth, rem-ceiling). */
h1 { font-size: clamp(2
...3. A Modular Type Scale
Picking font sizes at random gives a page that feels off. A modular scale picks one base size and one ratio, then multiplies up: each step is the previous size times the ratio. A ratio of 1.25 (the "major third") is a calm, common choice — 1rem, 1.25rem, 1.563rem, 1.953rem, and so on.
Store the steps as custom properties so the scale lives in one place and every heading just reads a token. Wrap each one in clamp() and you get a scale that is both consistent (the ratio) and fluid (the clamp).
Worked example: tokens built from one ratio
One base size and ratio drive the whole heading scale
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modular scale</title>
<style>
:root {
/* Base 1rem, ratio 1.25 (major third). Each step = previous x 1.25,
and each is clamped so it stays fluid AND readable. */
--step-0: clamp(1rem, 0.95rem + 0.3vw, 1.125rem); /* body */
--step-1: clamp(1.25rem, 1.1rem + 0.8vw, 1.563rem); /* h3 */
--step-2: clamp(1.563rem, 1.
...4. Line-height, Measure (ch) & text-wrap
Size is only half of readable text. Line-height is the vertical space between lines — set it unitless (e.g. line-height: 1.6) so it multiplies the font size and scales with it; a px line-height won't. Measure is the line length, and the comfortable range is about 45–75 characters. Cap it with max-width: 65ch, where 1ch is the width of a "0" in your font.
Finally, text-wrap: balance evens out the lines of a heading so you don't get one orphan word on its own line, while text-wrap: pretty tidies the last lines of long paragraphs. Use balance for headings, pretty for body text.
Worked example: measure, line-height and wrapping
Cap the line length and balance a ragged heading
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Measure & wrapping</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
h1 {
font-size: clamp(1.75rem, 4vw, 2.75rem);
color:#3b82f6;
max-width: 22ch; /* short measure for the headline */
text-wrap: balance; /* even out the lines, no lonely last w
...5. Respecting User Zoom
People with low vision raise their browser's base font size or zoom the page, and accessibility guidelines require text to stay usable when zoomed to 200%. The golden rule: every font size must contain at least one rem (or em). A pure vw size ignores zoom entirely; a clamp() with rem bounds keeps growing.
Keep body text at a minimum of 1rem (16px), never pin the root size in a way that fights the user, and your fluid type stays friendly to everyone. The worked example puts a zoom-safe and a zoom-hostile heading side by side.
Worked example: zoom-safe vs zoom-hostile
One heading grows with zoom; the other refuses to
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zoom & type</title>
<style>
body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
.card { background:#1e293b; padding:18px; border-radius:8px; margin:12px 0; }
.tag { color:#3b82f6; font-weight:600; font-size:12px; text-transform:uppercase; }
/* GOOD: rem floor + rem ceiling. Browser zoom and
...🎯 Your Turn #1 — Make a heading fluid
The heading below is frozen at one px size and the paragraph runs the full width. Convert the heading to a fluid clamp() and cap the paragraph's measure. Fill in the blanks marked ___, then run it and check the comments.
Your Turn #1: clamp() a heading + cap the measure
Replace a fixed px size with a fluid, accessible clamp()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Turn 1</title>
<style>
/* 🎯 YOUR TURN — fill in the blanks marked ___ */
body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
h1 {
/* 1) Make this fluid: min 1.75rem, grow at 5vw, max 3.5rem. */
font-size: ___; /* 👉 replace ___ with clamp(1.75rem, 5vw, 3.5rem) */
...🎯 Your Turn #2 — Tokenise a scale and tidy the wrapping
Add the two missing scale tokens, point the headings at them, and tidy the ragged wrapping with text-wrap. Use the right keyword for each: balance for the heading, pretty for the paragraph.
Your Turn #2: scale tokens + text-wrap
Fill in the type scale and choose the right wrapping
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Turn 2</title>
<style>
/* 🎯 YOUR TURN — fill in the blanks marked ___ */
:root {
/* 1) Add the two larger steps (ratio 1.25 from --step-1). */
--step-1: clamp(1.25rem, 1.1rem + 0.8vw, 1.563rem);
___ /* 👉 add: --step-2: clamp(1.563rem, 1.3rem + 1.4vw, 1.953rem); */
___ /* 👉 add: --step-3: clamp(1.953rem,
...🧩 Mini-Challenge — A responsive article header from scratch
Support is faded now — only an outline is given. Build a small article header that is fluid, consistently scaled, comfortably measured, and zoom-safe. Lean on the worked examples in sections 2–4 if you get stuck.
Mini-Challenge: responsive typography from scratch
Fluid scale + measure + wrapping, no hand-holding
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini-Challenge</title>
<style>
/* 🧩 MINI-CHALLENGE: a responsive article header
1. In :root, declare a small type scale as tokens (ratio your choice):
--title, --lead, --body — each wrapped in clamp(rem, vw, rem)
2. Style <h1> with var(--title), a short measure (~22ch) and
text-wrap: balance
3. Styl
...⚠️ Common Errors (and the fix)
- px font sizes break zoom. Setting body text in
px(e.g.font-size: 18px) ignores a user's chosen base size, so "larger text" settings do nothing. Fix: size text inrem—font-size: 1.125rem— and reserve px for borders and hairlines. - vw with no min/max.
font-size: 5vwturns microscopic on a phone, enormous on a monitor, and ignores zoom entirely. Fix: wrap it —clamp(1rem, 5vw, 3rem)— so the rem floor and ceiling keep it readable and zoom-friendly. - Lines too long (no measure cap). A paragraph with no
max-widthruns edge-to-edge on a wide screen; past ~75 characters the eye struggles to find the next line. Fix:max-width: 65chon your text containers. - Line-height in px.
line-height: 24pxdoesn't scale when the font grows, so big headings cramp. Fix: use a unitless multiplier —line-height: 1.6. - balance on a long paragraph.
text-wrap: balanceis for short text and browsers cap how many lines it touches, so it won't help body copy. Fix: usebalanceon headings andprettyon paragraphs.
📋 Quick Reference
| Goal | Use this |
|---|---|
| Accessible font size | font-size: 1.125rem; |
| Fluid heading (no media query) | font-size: clamp(2rem, 6vw, 4rem); |
| Floor / ceiling only | max(1rem, 4vw) / min(4vw, 3rem) |
| Type scale token | --step-2: clamp(1.5rem, 1.3rem + 1.4vw, 1.95rem); |
| Readable line height | line-height: 1.6; (unitless) |
| Cap line length (measure) | max-width: 65ch; |
| Balance a heading | text-wrap: balance; |
| Tidy a paragraph's last lines | text-wrap: pretty; |
❓ Frequently Asked Questions
Should I use rem, em, or px for font sizes?
Use rem for font sizes almost every time. A rem is always relative to the root (the <html> element), so 1rem is whatever the user's chosen base size is — usually 16px, but larger if they've turned font size up in their browser settings. That means rem text respects the user's preference. em is relative to the element's own font size, which is great for spacing that should track the text it sits next to (like padding inside a button) but surprising for font sizes because it compounds when nested. px is a fixed device pixel: fine for hairline borders and shadows, but it ignores the user's font preference, so never set body font size in px.
Why is font-size: 5vw a bad idea on its own?
A raw vw value is a pure percentage of the viewport width with no floor and no ceiling. On a 320px phone, 5vw is only 16px; on a tiny watch it becomes unreadable, and on a 2560px monitor it balloons to 128px. Worse, vw does not respond to browser zoom or the user's font-size preference at all, so a low-vision user who zooms in gets no larger text. The fix is to wrap it: clamp(1rem, 5vw + 0.5rem, 4rem). The rem-based min and max give you a readable floor and a sane ceiling, and because rem units honour zoom, the clamped value still grows when the user zooms.
What does clamp(min, preferred, max) actually compute?
clamp() returns the preferred (middle) value, but never lets it drop below min or rise above max. So clamp(1.5rem, 4vw, 3rem) says: try 4vw; if that comes out under 1.5rem use 1.5rem instead; if it comes out over 3rem use 3rem. It is exactly max(min, min(preferred, max)) written more readably. Put your accessible floor in the first slot, the fluid viewport-based growth in the middle, and your upper limit in the third — and you get smooth scaling between two breakpoints with no media queries.
What is the ch unit and why limit line length with it?
1ch is the width of the digit zero in the current font, so it's a rough stand-in for one character. Typographers find lines of roughly 45–75 characters the most comfortable to read; longer and the eye loses its place returning to the next line. Setting max-width: 65ch on your text containers caps the measure (line length) at about 65 characters regardless of how wide the screen is, which keeps long paragraphs readable on big monitors. Because ch is font-relative, the cap scales sensibly when the font size changes.
What does text-wrap: balance do, and when should I use pretty instead?
text-wrap: balance evens out the number of characters across every line of a block, so a two- or three-line heading doesn't end with one lonely word on the last line. It's meant for short text — headings, titles, pull quotes — and browsers only balance a handful of lines for performance. For long body paragraphs use text-wrap: pretty instead: it doesn't balance the whole block but it prevents orphans (a single short word stranded on the final line) and improves the last few lines. Rule of thumb: balance for headings, pretty for paragraphs.
Does using clamp() and vw break accessibility or zoom?
It can if you do it carelessly, but done right it's fine. The danger is a font size built only from viewport units, because vw ignores zoom. As long as your clamp() min and max are written in rem (or em), the text still grows when a user zooms or raises their base font size — WCAG requires text to be resizable up to 200% without loss of content. Keep body text at a minimum of 1rem, never set the root font size in a way that locks it (avoid html { font-size: 62.5% } pinned in px), and your fluid type stays accessible.
🎉 Lesson Complete
You can now build typography that's fluid, consistent and accessible. The essentials:
- ✅ Size text in
remso it respects the user's font preference; keeppxfor borders - ✅ Make it fluid with
clamp(rem-floor, vw-growth, rem-ceiling)— no media queries - ✅ Pick one base size and ratio for a modular type scale that feels deliberate
- ✅ Use unitless
line-heightand cap the measure at ~65ch - ✅
text-wrap: balancefor headings,prettyfor paragraphs - ✅ Every font size keeps a
remin it so browser zoom still works
Next up: Flexbox Advanced Patterns, where you'll lay out these beautifully-sized elements into flexible, responsive components.
Sign up for free to track which lessons you've completed and get learning reminders.