Topic 9.4
Fluid Typography and Spacing
In one line
Fluid type grows smoothly with the screen instead of jumping at breakpoints: font-size: clamp(1rem, 0.5rem + 2vw, 2rem) never goes below the minimum or above the maximum, and in between it scales with the viewport; the same idea gives fluid spacing and a whole type scale.
Think of it like this
A dimmer switch instead of an on/off switch. A normal light switch has two states, off and on; a dimmer slides smoothly between them, and it has a lowest and a highest setting. Breakpoint typography is the on/off switch: 18px, then suddenly 24px at 768px. Fluid typography is the dimmer: the text grows a little with every extra pixel of screen, between a smallest and a largest size you choose.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Fluid typography
- Text sizes that change smoothly with the screen width, between a minimum and a maximum.
- `clamp(min, preferred, max)`
- A CSS function that uses the preferred value but keeps it between the min and the max.
- `vw`
- Viewport width unit:
1vwis 1% of the viewport's width. - Slope
- How fast the size grows: how many pixels of font size per pixel of screen width.
- Intercept
- The fixed part added to the
vwpart, so the size starts at the right value on small screens. - Type scale
- A set of font sizes where each step is the previous one times the same ratio, like 1.25.
- Fluid spacing
- Padding, margins and gaps that grow with the screen using
clamp(). - `ch`
- A unit about the width of the "0" character in the current font;
65chis a comfortable line length.
Step by step
01Pick two points
Decide the size at a small screen and at a large screen. Example: body text 16px at 400px, 20px at 1200px; the main heading 28px at 400px, 48px at 1200px.
02Work out the slope and intercept
For the heading: it grows 20px over 800px, so slope = 20 ÷ 800 = 0.025, which is 2.5vw. At 400px, 2.5vw = 10px, so the intercept is 28 − 10 = 18px = 1.125rem.
slope = (48 - 28) / (1200 - 400) = 0.025 -> 2.5vw
intercept = 28 - 0.025 * 400 = 18px -> 1.125rem
result = clamp(1.75rem, 1.125rem + 2.5vw, 3rem)
check 800 = 18 + 0.025 * 800 = 38px (halfway, good)03Write the clamp
MIN and MAX are your two sizes in rem (28px = 1.75rem, 48px = 3rem). Below 400px the heading stays 28px; above 1200px it stays 48px; in between it slides.
h1 {
font-size: clamp(1.75rem, 1.125rem + 2.5vw, 3rem);
line-height: 1.1; /* big text needs tighter lines */
}
body {
font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);
line-height: 1.6;
}04Turn it into a scale
Store each step in a custom property so every component uses the same sizes. Headings grow faster than body text because their MAX is further from their MIN.
:root {
--step-0: clamp(1rem, 0.875rem + 0.5vw, 1.25rem); /* body */
--step-1: clamp(1.2rem, 1rem + 0.8vw, 1.6rem); /* h3 */
--step-2: clamp(1.44rem, 1.1rem + 1.4vw, 2.1rem); /* h2 */
--step-3: clamp(1.75rem, 1.125rem + 2.5vw, 3rem); /* h1 */
}
body { font-size: var(--step-0); }
h3 { font-size: var(--step-1); }
h2 { font-size: var(--step-2); }
h1 { font-size: var(--step-3); }05Do the same for spacing
Fluid gaps and padding keep proportions as the screen grows: tight on phones, roomy on desktops, without breakpoints.
:root {
--space-s: clamp(0.75rem, 0.6rem + 0.6vw, 1rem);
--space-m: clamp(1rem, 0.75rem + 1vw, 1.5rem);
--space-l: clamp(1.5rem, 1rem + 2vw, 3rem);
}
.section { padding-block: var(--space-l); }
.cards { gap: var(--space-m); }
.prose { max-width: 65ch; }Try it yourself
- 1
Change the slope
In the container example, change
2cqito4cqiand keep the rest. Predict the size at 600px (8 + 24 = 32px, so it hits the maximum already) and run to check. - 2
Design your own clamp
Work out a
clamp()for 20px at 400px and 30px at 1000px. (Slope 10 ÷ 600 ≈ 1.667cqi; intercept 20 − 6.667 = 13.333px ≈ 0.833rem.) Put it in the container example and check the 400px and 1000px lines. - 3
Zoom test
Run the last example full screen and zoom to 200%. Measure both lines in DevTools' Computed tab before and after. Which one doubled?
Code & diagrams
The preview is narrow, so you'll likely see the minimum sizes. Open the full-screen editor and drag the window wider: the heading grows smoothly, then stops at 48px.
Preview
Console
Nothing logged yet.To get the same numbers on every screen, this example measures a box instead of the window: `cqi` (Topic 9.6) is 1% of the box's width, exactly like `vw` is 1% of the window's. Below 400px the minimum wins; above 1200px the maximum wins.
Preview
Console
Nothing logged yet.Expected console output
300px wide -> 16px
400px wide -> 16px
600px wide -> 20px
800px wide -> 24px
1000px wide -> 28px
1200px wide -> 32px
1400px wide -> 32pxEvery size and gap comes from a handful of variables. Change `--step-3` once and every page heading follows.
Preview
Console
Nothing logged yet.Open this full screen and zoom the browser to 200% (Ctrl/Cmd and +). The green line grows; the red one barely changes, because zooming shrinks the CSS viewport and `5vw` shrinks with it. On a narrow phone the red text is also tiny.
Preview
Console
Nothing logged yet.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
MIN bigger than MAX
Write font-size: clamp(2rem, 1rem + 2vw, 1rem) by mistake.
Break #2
Forgetting a unit in the maths
Write font-size: clamp(1rem, 0.5 + 2vw, 2rem).
Myth vs fact
Myth
Fluid type means using vw for font sizes.
Fact
Pure vw breaks zoom and makes text tiny on phones. Fluid type is vw mixed with rem and wrapped in clamp() with sensible limits.
Myth
With fluid type you don't need media queries any more.
Fact
Fluid type removes breakpoints for sizes, but layout changes (sidebar or no sidebar, menu style) still use media or container queries.
Myth
Bigger screens should always have bigger body text.
Fact
Body text only needs to grow a little (16 to 20px is plenty). Headings can grow more. Line length (max-width: 65ch) matters more for readability than font size.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
clamp()resolves against the element's font context:remuses the root font size,emthe parent's (forfont-size). Usingemin a fluidfont-sizecompounds through nested elements; useremfor the bounds and intercept. - ▸
Containers matter more than windows for components. Swapping
vwforcqiin a component'sclamp()makes the text follow the card's width, so the same card works in a sidebar and in the main column (Topic 9.6). If there's no container ancestor,cqifalls back to the small viewport unitsvi. - ▸
The 2.5× guideline (from accessibility specialist Adrian Roselli's testing) comes from the maths of zoom: zooming shrinks the viewport measured in CSS px, so the
vwpart shrinks while the zoom enlarges everything. If MAX is far above MIN, zoom pushes the computed size back down towards MIN and cancels much of the zoom, so the text may never reach twice its size, even at the browser's maximum zoom. Test with real zoom. - ▸
Some properties look better un-fluid:
line-heightshould be unitless (Phase 4) so it tracks the fluid font size automatically, and letter-spacing inemscales too. Big display headings often need negative letter-spacing (-0.02em) and tighter line-height.
Remember this
- 1
The tool is
clamp(MIN, PREFERRED, MAX)from Phase 3 (Topic 3.7). It returns the PREFERRED value, but never less than MIN and never more than MAX. For fluid type, the preferred value mixes a fixed part and a part that depends on the viewport:clamp(1rem, 0.5rem + 2vw, 2rem).vwis 1% of the viewport width (Phase 3, Topic 3.3), so2vwis 8px on a 400px phone and 24px on a 1200px laptop. - 2
Work out the preferred value with a little maths, so the size hits MIN at a small screen and MAX at a large one. Say you want 16px at 400px wide and 32px at 1200px wide. The size must rise by 16px over 800px of width: slope = 16 ÷ 800 = 0.02, which is
2vw(2% of the width). At 400px,2vwis 8px, so you need 8px more to reach 16px: that's the intercept,0.5rem. Result:clamp(1rem, 0.5rem + 2vw, 2rem). Check at 800px: 8 + 16 = 24px, halfway, as expected. - 3
The general recipe:
slope = (maxSize − minSize) ÷ (maxWidth − minWidth),vw part = slope × 100,intercept = minSize − slope × minWidth. Write the intercept inremso it follows the user's font-size setting. Online calculators (search "fluid type calculator" or "Utopia") do this for you, but you should be able to check their output. - 4
Accessibility matters here. A font size in pure
vw(font-size: 4vw) ignores browser zoom: zooming changes the CSS viewport width in a way that keeps4vwalmost the same visible size, so the text barely grows, which fails WCAG 1.4.4 (text must be resizable to 200%). Two habits fix it: always include arempart in the preferred value, and keep MAX no more than about 2.5 times MIN, so that zooming can still make the text at least twice as big. Therembounds also mean a user's larger default font raises both limits. - 5
A type scale is a set of related sizes (small, body, h3, h2, h1) where each step is the previous times a ratio. On small screens a gentle ratio (like 1.2) keeps headings from crowding the phone; on large screens a bigger ratio (like 1.333) gives drama. Fluid scales clamp each step between its small-screen and large-screen size, stored in custom properties:
--step-0,--step-1,--step-2. Headings then grow faster than body text, which is what you want. - 6
Fluid spacing uses the same trick for padding, margins and gaps:
--space-m: clamp(1rem, 0.75rem + 1vw, 1.5rem). Spacing that scales with the screen keeps layouts feeling balanced without a breakpoint for every gap. For components inside cards and sidebars, you can swapvwfor container units likecqi(Topic 9.6) so the text follows the component's width instead of the window's. Line length matters too: keep body text around 45–75 characters per line withmax-width: 65ch.
Explain it without notes
How does clamp() create fluid typography? Walk through the three arguments.
Derive the clamp() for text that is 18px at 360px wide and 24px at 1200px wide.
Why is font-size: 3vw an accessibility problem, and how do you avoid it?
What is a fluid type scale, and why might headings grow faster than body text?
Practice
Write a clamp() for an h2 that is 24px at 400px wide and 36px at 1200px wide. Show your working.
Create three fluid spacing variables (small, medium, large) and use them for a card's padding, the gap of a grid, and a section's vertical padding.
A designer asks for the hero title to be 8vw. Rewrite it so it still feels big and fluid but works with zoom and doesn't get tiny on phones.
Trade-offs
- ↔
Fluid sizes remove many breakpoints and look polished at every width, but exact sizes at a given width are harder to predict and to match to a pixel-perfect design file.
- ↔
A full fluid type and space scale gives consistency, but it adds variables everyone must learn; small sites can do well with fluid headings and fixed body text.
Done when you can
I can explain
clamp(MIN, PREFERRED, MAX)and what happens at each end.I can calculate slope and intercept from two sizes at two widths.
I always include a
rempart and keep MAX within about 2.5× MIN.I can build a fluid type scale with custom properties.
I can make padding and gaps fluid too.
I keep body text to about 65ch per line.