Section 3 · Responsive and Robust
Fluid Type and Space
17 min3 min readCode playground
Lesson at a glance
Build fluid type and spacing with clamp(), calculate the slope and intercept from two design sizes, and keep text resizable for users who change their font size.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Waypoint’s hero heading, “Two days of front-end, in Lisbon”, is 48px on the design for a laptop and 28px on the design for a phone. The first implementation had three media queries and the heading jumped between three sizes as you resized. Nobody resizes a window for fun, but they do rotate tablets, split screens and open the page in narrow side panels, and every jump in between looked accidental.
Fluid type replaces the jumps with a straight line between the two designs.
A fluid value is a line
You have two points from the design: at a 20rem (320px) viewport the heading is 1.75rem; at 60rem (960px) it’s 3rem. Between them you want it to grow in proportion; outside them, it should stop. That’s clamp() with a linear preferred value:
.hero h1 {
font-size: clamp(1.75rem, 1.125rem + 3.125vw, 3rem);
}The middle value is a straight line, intercept + slope × width. The slope is how much the size changes per unit of width: (3 − 1.75) ÷ (60 − 20) = 0.03125, which as a viewport unit is 3.125vw. The intercept is where that line crosses zero width: 1.75 − 0.03125 × 20 = 1.125rem. The calculation is easy to get wrong by hand, so here it is as a function you can run:
function fluid(minSize, maxSize, minWidth, maxWidth, unit = 'vw') {
// sizes and widths in rem
const slope = (maxSize - minSize) / (maxWidth - minWidth);
const intercept = minSize - slope * minWidth;
const round = (n) => Math.round(n * 1000) / 1000;
return `clamp(${minSize}rem, ${round(intercept)}rem + ${round(slope * 100)}${unit}, ${maxSize}rem)`;
}
console.log(fluid(1.75, 3, 20, 60)); // hero heading
console.log(fluid(1, 1.75, 20, 50, 'cqi')); // card title, container-basedTry other pairs. Two design sizes in, one clamp() out.
Why the rem matters
You could write font-size: 5vw and call it fluid. Don’t. Viewport units know nothing about the user’s font preferences: someone who sets their browser’s default text size to 20px gets exactly the same 5vw as everyone else. WCAG’s Resize Text criterion asks that text can scale to 200% without loss of content, and type that ignores the user’s settings fails people who need it most.
Including a rem term keeps the user in the equation. With 1.125rem + 3.125vw, the rem part grows when the user raises their default size, and the rem limits move with it.
There’s a mathematical edge to watch: if the ratio between MAX and MIN is large and the slope is steep, zooming can shrink the effective range so much that 200% zoom doesn’t produce 200% text. Keep the max no more than about 2.5 times the min for body copy and headings, and you’re safe.
Viewport or container?
vw is right for type tied to the page: the hero heading, the page title. For type inside a component, use container units. A session card’s title in the 280px sidebar and in the 640px main column should differ, and vw can’t tell them apart. With the slot as a size container (from the previous lesson), the same formula works with cqi:
.slot { container-type: inline-size; }
.session-card h3 {
font-size: clamp(1rem, 0.5rem + 2.5cqi, 1.75rem);
}Fluid space
Type that scales while padding stays fixed looks wrong at both ends: cramped on big screens, airy on small ones. Define a few spacing tokens with the same technique and use them everywhere:
:root {
--space-s: clamp(0.5rem, 0.4rem + 0.5vw, 0.75rem);
--space-m: clamp(1rem, 0.75rem + 1.25vw, 1.5rem);
--space-l: clamp(1.5rem, 1rem + 2.5vw, 3rem);
}
.session-card { padding: var(--space-m); }
.schedule { gap: var(--space-l); }Three or four steps are enough. More than that and nobody remembers which to use.
Your turn
The exercise’s card title uses 5cqi, which grows without limits and ignores the user’s font size. Replace it with a clamp() that hits 16px in a 320px slot and 28px in an 800px slot, with rem limits. The checks change the slot width and even the root font size. Next, you’ll make the same page work in Arabic, right to left, with logical properties.
Key takeaways
- A fluid value is a straight line between two points:
clamp(MIN, intercept + slope × width, MAX), where the slope is the size change divided by the width change. - Put
remin the intercept and the limits so text still responds to the user’s default font size and to zoom; viewport-only sizes don’t. - Use
vwfor page-level type such as the hero heading, andcqifor type inside components that live in containers of different widths. - Fluid spacing tokens such as
--space-mkeep padding and gaps proportional to type without a breakpoint for each.