Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~27 min· topic 4 of 7

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: 1vw is 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 vw part, 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; 65ch is 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.

maths.txtwhole filetext
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.

style.csswhole filecss
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;
}
Write the clampdiagram
Rendering diagram…

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.

style.csswhole filecss
: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.

style.csswhole filecss
: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. 1

    Change the slope

    In the container example, change 2cqi to 4cqi and keep the rest. Predict the size at 600px (8 + 24 = 32px, so it hits the maximum already) and run to check.

  2. 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. 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

A fluid heading (resize the window) New tab

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

Press Run to see the page.

Console

Nothing logged yet.
Proving the maths with a container New tab

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

Press Run to see the page.

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 -> 32px
A fluid type scale and spacing New tab

Every size and gap comes from a handful of variables. Change `--step-3` once and every page heading follows.

Preview

Press Run to see the page.

Console

Nothing logged yet.
Why pure vw text is a problem New tab

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

Press Run to see the page.

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.

terminal
$ Computed font-size
── what you'll see ──
Always 32px (2rem) at every width: the text never shrinks or grows.

Break #2

Forgetting a unit in the maths

Write font-size: clamp(1rem, 0.5 + 2vw, 2rem).

terminal
$ DevTools Styles pane
── what you'll see ──
The declaration is crossed out with a warning icon (invalid property value); the text falls back to its inherited size.

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: rem uses the root font size, em the parent's (for font-size). Using em in a fluid font-size compounds through nested elements; use rem for the bounds and intercept.

  • ▸

    Containers matter more than windows for components. Swapping vw for cqi in a component's clamp() 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, cqi falls back to the small viewport unit svi.

  • ▸

    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 vw part 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-height should be unitless (Phase 4) so it tracks the fluid font size automatically, and letter-spacing in em scales too. Big display headings often need negative letter-spacing (-0.02em) and tighter line-height.

Remember this

  1. 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). vw is 1% of the viewport width (Phase 3, Topic 3.3), so 2vw is 8px on a 400px phone and 24px on a 1200px laptop.

  2. 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, 2vw is 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. 3

    The general recipe: slope = (maxSize − minSize) ÷ (maxWidth − minWidth), vw part = slope × 100, intercept = minSize − slope × minWidth. Write the intercept in rem so 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. 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 keeps 4vw almost 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 a rem part 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. The rem bounds also mean a user's larger default font raises both limits.

  5. 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. 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 swap vw for container units like cqi (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 with max-width: 65ch.

Explain it without notes

01

How does clamp() create fluid typography? Walk through the three arguments.

02

Derive the clamp() for text that is 18px at 360px wide and 24px at 1200px wide.

03

Why is font-size: 3vw an accessibility problem, and how do you avoid it?

04

What is a fluid type scale, and why might headings grow faster than body text?

Practice

01

Write a clamp() for an h2 that is 24px at 400px wide and 36px at 1200px wide. Show your working.

02

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.

03

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 rem part 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.