A type scale built on media queries jumps. The heading is 28px at 767 pixels of viewport width and 40px at 768, and nothing in between looks deliberate. Fluid typography replaces the jump with a continuous slope: the font size grows smoothly from a minimum at a small viewport to a maximum at a large one. CSS clamp() is the function that makes this a single declaration, and it has been Widely Available across browsers since July 2020, so there is no support caveat to manage. The real constraint is accessibility, and it is easy to get wrong.
How clamp() Works
clamp(min, preferred, max) resolves to the preferred value, bounded below by the minimum and above by the maximum. MDN defines it as equivalent to max(min, min(preferred, max)). The three arguments can use different units and can contain math, which is what makes fluid type possible:
h1 {
font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
}
Here the minimum is 1.5rem and the maximum is 3rem. The preferred value, 1rem + 2.5vw, is the slope between them. While the viewport is narrow, the preferred value is smaller than 1.5rem and the floor applies. While it is wide, the preferred value passes 3rem and the ceiling applies. Between those two viewport widths, the size changes continuously with no breakpoint.
Deriving the Preferred Value
The preferred value is a line: a constant plus a multiple of the viewport width. Pick the two viewport widths where the scaling should start and stop, and the two font sizes you want at those widths. With sizes in pixels:
- slope = (max size − min size) ÷ (max viewport − min viewport)
- intercept = min size − slope × min viewport
Multiply the slope by 100 to express it in vw, and divide the intercept by 16 to express it in rem, assuming the default 16px root size.
For the heading above, take 24px at a 320px viewport and 48px at 1280px. The slope is 24 ÷ 960 = 0.025, which is 2.5vw. The intercept is 24 − 0.025 × 320 = 16px, which is 1rem. That is where 1rem + 2.5vw comes from. You can check it: at 1280px, 16px + 32px gives 48px, exactly the maximum.
Applying the same method across a small scale gives values you can paste:
:root {
--step-0: clamp(1rem, 0.9167rem + 0.4167vw, 1.25rem); /* body */
--step-1: clamp(1.25rem, 1rem + 1.25vw, 2rem); /* h2 */
--step-2: clamp(1.5rem, 1rem + 2.5vw, 3rem); /* h1 */
}
body { font-size: var(--step-0); line-height: 1.6; }
h2 { font-size: var(--step-1); line-height: 1.25; }
h1 { font-size: var(--step-2); line-height: 1.1; }
Each step scales between a 320px and a 1280px viewport. If your design needs different endpoints, rerun the arithmetic rather than adjusting the numbers by eye. Calculators such as the Utopia project do the same computation and can generate a full scale, but the formula is short enough to keep in your head, and understanding it makes the output easier to audit.
The Zoom Problem
This is where fluid type earns its reputation for accessibility trouble. WCAG 2.2 Success Criterion 1.4.4, Resize Text (Level AA), requires that text can be resized up to 200 percent without assistive technology and without loss of content or functionality.
Browser zoom works by shrinking the CSS viewport. At 200 percent zoom, a 1280px-wide window behaves like a 640px one. A rem-based size doubles, as expected. A vw size does not: 2.5vw is computed from the shrunken viewport, so the text can end up no larger than it was before the user zoomed, or barely larger. A font size written only in vw can defeat zoom entirely. The Smashing Magazine analysis of this problem, which builds on work by Adrian Roselli and Maxwell Barvian, also points out that capping growth with clamp() or max() can itself cause a 1.4.4 failure, because the ceiling limits how large the text can get.
Two habits keep you on the right side:
- Never use a bare
vwpreferred value. Always include a rem term, as every example above does. The rem portion scales with zoom and with the user’s own font-size preference; thevwportion only shapes the slope. - Keep the ratio between your maximum and minimum modest. The same Smashing analysis concludes that, to pass reliably across modern browsers, the maximum size should be no more than 2.5 times the minimum. Its example of a failing declaration,
clamp(16px, 5.33px + 3.33vw, 48px), has a ratio of 3 and fails at 500 percent zoom within a band of viewport widths. The scales above stay at or below a 2× ratio.
Treat the ratio as a safety margin, not a substitute for testing. Load the page, zoom to 200 percent and then 500 percent at several window widths, and confirm the text is visibly larger than at 100 percent each time.
Practical Guidance for a Real Stylesheet
Fluid scaling suits display text most. Headings and hero copy gain the most from continuous sizing, while body text often needs only a small range, which is why --step-0 spans just 1rem to 1.25rem. Some teams keep body copy at a fixed 1rem and apply fluid sizing only to headings, which removes most of the zoom risk in the text people read the longest.
Set line height as a unitless number so it follows whatever size clamp() produces. Fluid spacing follows the same pattern, since padding and margins can use the identical formula, but check that a large minimum does not cause horizontal overflow on narrow screens.
Finally, keep the scale in custom properties, as shown, so the intent is readable and a change to the endpoints happens in one place. If you want a fallback for very old browsers, declare a fixed font-size before the clamp() line; a browser that does not understand the function ignores the second declaration and keeps the first.
The working method is short: decide your two viewport endpoints, compute each step’s slope and intercept, keep a rem term in every preferred value, hold the maximum near twice the minimum, and then zoom the page to verify. Do that once, store the results as custom properties, and your type scale stays smooth at every width without a single typography breakpoint.



