Accessibility
Fluid typography and accessibility
How to make clamp() font sizes that still grow when people zoom in or change their default font size, and how to test them.
Fluid type scales with the screen. That is the point of it, but it also means the font size depends on something the reader does not control directly. Done badly, text stops growing when people zoom in. Done well, a clamp() font size is just as accessible as a fixed one.
What WCAG asks for
WCAG success criterion 1.4.4, Resize Text, is level AA. It says that, except for captions and images of text, text can be resized without assistive technology up to 200 percent without loss of content or functionality.
In practice most people resize text with browser zoom, and some change the default font size in their browser settings. Your fluid type has to work with both.
How browser zoom works
Browser zoom makes every CSS pixel bigger. The window on the screen stays the same size, so the page gets fewer CSS pixels wide. A 1440px wide window at 200% zoom is a 720px wide page as far as your CSS is concerned.
That is also why a page can switch to its mobile layout when you zoom in far enough.
Why pure vw font sizes fail
A vw unit is 1% of the viewport width in CSS pixels. Take a heading set in viewport units only:
h1 {
font-size: 4vw;
}In a 1440px window at 100%, that is 57.6px. At 200% zoom the page is 720px wide, so 4vw is 28.8 CSS pixels. Each of those is drawn twice as large, so the heading is still 57.6px on screen. Zoom enlarged the pixels and shrank the viewport by the same factor, and the two cancel out.
The heading does not grow at all, however far the reader zooms. It also ignores the browser’s default font size setting, because vw has nothing to do with font sizes.
Why the rem part matters
A clamp() font size mixes units. Take this heading, which grows from 36px to 64px between a 360px and a 1440px screen:
h1 {
font-size: clamp(2.25rem, 1.667rem + 2.593vw, 4rem);
}When you zoom in, the two parts of the preferred value behave differently. The 1.667rem part grows with zoom, like any fixed size. The 2.593vw part stays the same size on screen, like the pure vw heading above. The minimum and maximum are in rem, so they grow with zoom too.
So the larger the rem share of the preferred value, the better the text responds to zoom. A small size range over a wide viewport range gives a small slope and a large rem part. A big size range over a narrow viewport range does the opposite.
Here is what that heading does in a 1440px wide window:
- 100% zoom: 64px, the maximum.
- 200% zoom: about 91px on screen. That is 1.42 times the original, not 2 times.
- About 340% zoom: 128px, twice the original.
- 500% zoom: 180px, the minimum of 36px drawn five times as large.
So even a reasonable fluid heading may not double at exactly 200% zoom. It gets there, but it needs more zoom than a fixed size would. The deep dive explains the formula behind these numbers.
The 2.5 times rule of thumb
A common rule is to keep the maximum font size at or below about 2.5 times the minimum. Here is the reasoning behind it:
- Chrome, Edge and Firefox let you zoom up to 500%.
- At any zoom level, the text is never smaller than its minimum in CSS pixels. At 500% zoom, that minimum is drawn five times as large.
- The text is never larger than its maximum at 100% zoom. To reach 200% of any size it had, it has to reach twice the maximum.
- Five times the minimum is at least twice the maximum when the maximum is at most 2.5 times the minimum.
The heading above passes: 64px is about 1.8 times 36px. Now take a heading that grows from 16px to 64px over the same range. The intercept works out to zero, so the preferred value is only viewport units:
h1 {
font-size: clamp(1rem, 4.444vw, 4rem);
}The maximum is four times the minimum. In a 1440px window it stays at 64px on screen at 100%, 200% and 400% zoom, and only reaches 80px at 500%. It never gets close to 128px.
A few things to keep in mind about this rule:
- It is a safe upper limit, not an exact test. Values above 2.5 times can still pass, depending on the slope and the window size.
- It relies on the browser zooming to 500%. If a browser stops earlier, the safe ratio is lower.
- It counts text as resizable if it reaches 200% at some zoom level. WCAG does not say which zoom level has to get you there, and a stricter reading expects twice the size at 200% zoom. As the example above shows, most fluid headings do not meet that stricter reading.
Two articles cover this in depth. Adrian Roselli’s Responsive Type and Zoom shows how viewport-based type fails to scale when zoomed. Maxwell Barvian’s Addressing Accessibility Concerns With Using Fluid Type on Smashing Magazine works through the maths and the 2.5 times rule.
Respect the default font size
Browser zoom is not the only way people make text bigger. Many set a larger default font size in their browser settings, so every site starts larger. That setting changes the size of 1rem, and nothing else.
- Use rem, not px, for the minimum, maximum and intercept. A value like
clamp(16px, 10px + 1.563vw, 40px)scales with zoom but ignores the font size setting completely. - Do not set a px font size on the root element.
html { font-size: 16px; }overrides the user’s setting for everyremvalue on the page. - Keep body text close to fixed. A range like
1remto1.125rembarely depends on the viewport, so it behaves almost exactly like a fixed size.
The clamp() generator outputs rem for every fixed part of the value, so this is taken care of if you copy its output.
Avoid loss of content
The second half of 1.4.4 is easy to forget: text has to grow without loss of content. At high zoom levels the page is very narrow in CSS pixels, so it behaves like a small phone. Make sure your minimum size still fits there:
- Long words in large headings should wrap instead of overflowing the screen.
- Boxes with a fixed height should not clip text that has grown.
- Sticky headers should not cover most of the screen when zoomed in.
How to test
- Open the page in a wide window and zoom to 200% with
CtrlorCmdand+. Check that headings and body text are clearly larger, and that nothing overlaps, overflows or gets cut off. - Keep zooming towards 400% and 500%. Your largest heading should eventually reach twice the size it had at 100%.
- To compare exact sizes, read the computed
font-sizein DevTools. It is in CSS pixels, so multiply it by the zoom level to get the size on screen. - Reset the zoom and raise the default font size in the browser settings. In Chrome that is under Appearance, in Firefox under Fonts. All text should grow, including fluid headings.
- Repeat the zoom test in a narrow window, where most text is at its minimum.
Summary
- Never size text with viewport units alone.
- Use
remfor the minimum, the maximum and the intercept. - Keep the maximum at or below about 2.5 times the minimum.
- Test at 200% zoom and beyond, and with a larger default font size.
Fluid typography accessibility FAQ
Is fluid typography with clamp() accessible?
It can be. Use rem for the minimum, the maximum and the fixed part of the preferred value, keep the maximum within about 2.5 times the minimum, and test at 200% browser zoom. Font sizes made only of vw units are the ones that fail.
Why does a vw font size not grow when I zoom in?
Browser zoom makes CSS pixels larger, so the page gets fewer CSS pixels wide. A vw value is a percentage of that width, so it shrinks in CSS pixels by the same factor that zoom enlarges them. The two cancel out and the text stays the same size on screen.
What does WCAG 1.4.4 Resize Text require?
Success criterion 1.4.4, level AA, requires that text can be resized up to 200 percent without assistive technology and without loss of content or functionality. Captions and images of text are excluded.
What is the 2.5 times rule for fluid type?
If the maximum font size is at most 2.5 times the minimum, text can reach twice its largest size at 500% browser zoom, because at that zoom it renders at no less than 5 times its minimum. It is a conservative rule of thumb, not part of WCAG.
Should fluid font sizes use rem or px?
Use rem. Both scale with browser zoom, but only rem also follows the default font size a user sets in their browser settings. A clamp() with px bounds ignores that setting completely.