Skip to main content
Back to module
Accessible & Responsive Interfaces · Responsive Design

Fluid type and spacing

Reading48mLesson 1 of 3 in this module

Use rem, clamp() and spacing tokens so type and space scale with the screen and the reader's settings.

Why this matters

Your page will be read on a 320 px phone, a 1440 px laptop and a 4K monitor, by people who have set their browser's default text size larger, or who zoom to 200 or 400 percent. Fixed pixel sizes fight all of that. Relative units and clamp() let type and spacing scale smoothly with both the screen and the reader's own settings, which is good design and a WCAG requirement (1.4.4 Resize Text and 1.4.10 Reflow, both AA).

Relative units, briefly

UnitRelative toGood for
remThe root (html) font size, 16 px by defaultFont sizes, spacing, breakpoints
emThe current element's font sizePadding on buttons that should grow with their text
chThe width of the "0" characterLine length (max-width: 65ch)
%The parent's sizeWidths inside a container
vw / vh1 percent of the viewport width / heightThe fluid part of a clamp()
dvh, svh, lvhDynamic, small and large viewport heightsFull-height sections on mobile, where browser toolbars come and go

The most important rule: never set the root font size in pixels. html { font-size: 16px; } overrides the reader's browser setting. If someone has chosen 20 px default text because of low vision, you just took it away. Leave the root alone, or use a percentage (font-size: 100%), and size everything else in rem.

clamp(): fluid with limits

clamp(MIN, PREFERRED, MAX) returns the preferred value, but never less than MIN or more than MAX:

h1 {
  font-size: clamp(2rem, 1.5rem + 2.5vw, 3.5rem);
}
  • On a narrow phone, 1.5rem + 2.5vw is small, so the heading stops at 2rem (32 px by default).
  • As the window grows, the vw part grows, and the heading scales smoothly.
  • On a wide screen it stops at 3.5rem.

Why 1.5rem + 2.5vw and not just 5vw? Because viewport units do not respond to text zoom. With pure vw, a user pressing Ctrl and + sees the layout shift but the heading stays the same size, which can fail 1.4.4. Mixing a rem term into the preferred value keeps it zoomable. Keep the MAX no more than about 2.5 times the MIN so 200 percent zoom still makes a real difference.

min() and max() are the one-sided versions: width: min(100%, 60rem) means "fill the space, but never wider than 60rem".

Fluid spacing with tokens

Spacing should scale too, or a big heading ends up cramped on a laptop and enormous gaps eat a phone screen. Define a small set of spacing tokens as custom properties, then use only those:

:root {
  --space-s: clamp(0.75rem, 0.7rem + 0.25vw, 1rem);
  --space-m: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
  --space-l: clamp(1.5rem, 1.2rem + 1.5vw, 3rem);
}

.card { padding: var(--space-m); }
.stack > * + * { margin-block-start: var(--space-s); }

Tokens keep a design consistent and make changes a one-line edit. Design systems call these "design tokens", and they are often generated from the same values designers use in Figma. Tools such as the free Utopia calculator (utopia.fyi) produce matching type and space scales for you.

Readable line length

Long lines are hard to track back to the start of the next line, especially for people with dyslexia or low vision. Limit body text to roughly 45 to 75 characters:

.prose p { max-width: 65ch; }
body { line-height: 1.5; }

line-height without a unit (1.5) scales with the font size. A unit-less value is also what WCAG 1.4.12 Text Spacing expects users to be able to override without breaking the layout, so avoid fixed heights on text containers.

Worked example: converting a fixed design

A designer hands you a desktop mock-up: 48 px heading, 18 px body, 24 px card padding, 960 px content width. A fixed translation would be:

html { font-size: 16px; }
h1 { font-size: 48px; }
.card { padding: 24px; width: 960px; }

A fluid, accessible translation:

h1 { font-size: clamp(2rem, 1.5rem + 2.5vw, 3rem); }  /* 32 px to 48 px */
body { font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem); line-height: 1.5; } /* 16 to 18 */
.card { padding: var(--space-m); }
.container { width: min(100% - 2rem, 60rem); margin-inline: auto; }

The desktop result is the same as the mock-up. The phone version is no longer squeezed, and every size follows the reader's preferences.

Common mistakes

  • Setting html { font-size: 62.5%; } or a pixel value to make "1rem = 10px". It still works with user settings if it is a percentage, but it confuses teams; prefer the default.
  • Pure vw font sizes, which ignore zoom.
  • A clamp() with MIN larger than MAX (MIN wins, silently).
  • Fixed height on text boxes, which clips text when users increase spacing.
  • Too many one-off sizes instead of a token scale.

Quick recap

  • Size type and spacing in rem; never fix the root font size in px.
  • clamp(min, rem + vw, max) gives fluid sizes that still zoom.
  • Put spacing in a few custom-property tokens.
  • Cap paragraphs at about 65ch and use a unit-less line-height.
  • Reference: MDN: clamp() and WCAG Understanding 1.4.4 Resize Text.

Try it

Open the Do tab and convert a fixed-pixel page to fluid type and spacing.

Also in this lesson: