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
| Unit | Relative to | Good for |
|---|---|---|
rem | The root (html) font size, 16 px by default | Font sizes, spacing, breakpoints |
em | The current element's font size | Padding on buttons that should grow with their text |
ch | The width of the "0" character | Line length (max-width: 65ch) |
% | The parent's size | Widths inside a container |
vw / vh | 1 percent of the viewport width / height | The fluid part of a clamp() |
dvh, svh, lvh | Dynamic, small and large viewport heights | Full-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.5vwis small, so the heading stops at2rem(32 px by default). - As the window grows, the
vwpart 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
vwfont sizes, which ignore zoom. - A
clamp()with MIN larger than MAX (MIN wins, silently). - Fixed
heighton 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
65chand use a unit-lessline-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.