Skip to content

How CSS clamp() Replaces Your Media Query Stack for Type

How to replace media query breakpoints with CSS clamp() for fluid type: the formula, worked examples, browser support, and the too-small mobile pitfall.

· · 6 min read
Multiple computer monitors on a desk showing an interface at different sizes side by side

I inherited a marketing site with six separate media queries, each one nudging the hero H1 down by a few pixels for a slightly different breakpoint. Somebody added a seventh for an oddball 834px iPad viewport nobody remembered testing. On a project last year I "fixed" a client's heading scale with clamp() instead, and I set the minimum too aggressively. QA caught it on an actual iPhone SE during a walkthrough: the body copy had shrunk to something close to 13px and read like a legal disclaimer.

TL;DR: CSS clamp() replaces a breakpoint stack with one formula: minimum, preferred (viewport-relative), maximum. Global browser support sits at 94.2 percent (Can I Use, 2026), but an aggressive minimum can fail WCAG 1.4.4 text-resize checks well before you hit desktop widths.

Blue and orange sky, one colour ramping continuously into the other
Photo by Andrew Ruiz on Unsplash

How Does the clamp() Three-Argument Syntax Actually Work?

clamp() takes exactly three arguments, in order: a minimum, a preferred value, and a maximum. The browser picks whichever of the three keeps the result inside the min/max bounds.

h1 {
  font-size: clamp(1.75rem, 1.333rem + 1.667vw, 3rem);
}

Read that as: never go below 1.75rem (28px), never go above 3rem (48px), and between those two limits, scale using the middle expression. The middle argument is where the actual fluidity lives, it mixes a fixed rem value with a viewport-relative vw value, so the size grows as the browser window widens and shrinks as it narrows. Below the minimum viewport, you get the minimum. Above the maximum viewport, you get the maximum. In between, it's linear.

A metric scale marked out in fine, even increments
Photo by Ries Bosch on Unsplash

How Do You Compute the vw Coefficient for the Slope Formula?

This is the part most tutorials wave their hands at, so here's the actual math. Say you want body text to scale from 28px at a 400px viewport up to 48px at a 1600px viewport.

slope = (maxSize - minSize) / (maxViewport - minViewport)
slope = (48 - 28) / (1600 - 400) = 20 / 1200 = 0.016667

That slope is pixels of font size per pixel of viewport width. Multiply by 100 to convert it into a vw coefficient: 1.667vw. Next you need the y-intercept, the rem value the formula would produce at a viewport of zero:

yIntercept = minSize - (slope * minViewport)
yIntercept = 28 - (0.016667 * 400) = 28 - 6.667 = 21.333px = 1.333rem

Put those two numbers together and you get the exact expression from the code block above: clamp(1.75rem, 1.333rem + 1.667vw, 3rem). Check the math at both ends: at a 400px viewport, 1.333rem (21.33px) plus 1.667vw of 400px (6.67px) equals 28px, matching the minimum exactly. At 1600px, 21.33px plus 1.667vw of 1600px (26.67px) equals 48px, matching the maximum. That's the whole trick, it's just linear interpolation dressed up in CSS syntax.

ViewportFormula resultMatches
400px (minimum)1.333rem + 1.667vw of 400px = 28pxMinimum bound
1600px (maximum)1.333rem + 1.667vw of 1600px = 48pxMaximum bound
Photo by Chris Ried on Unsplash

What Happens When Your Minimum Size Is Too Aggressive?

This is the mistake I made, and I've since seen at least three other teams make the same one. A clamp() minimum that looks fine in a design tool at desktop width can be genuinely unreadable once it actually renders on a 375px phone in someone's hand. Below 14px, even a well-built typeface starts failing for readers with mild low vision, and 16px is where most accessibility guidance draws the practical floor for anything meant to be read at length.

There's a second, sneakier failure mode buried in the spec itself. Because clamp() with a vw component doesn't respond to browser zoom the way a plain rem value does, an aggressive max-to-min ratio can leave large text stuck at a size that never reaches 200 percent when a user zooms in, which is exactly the threshold WCAG Success Criterion 1.4.4 requires. The guidance from Smashing Magazine's accessibility review of fluid type puts a hard number on it: keep your maximum at or below 2.5 times your minimum, or the resize math stops adding up at certain viewport widths (Smashing Magazine, 2023).

Photo by Claudio Schwarz on Unsplash

Is Browser Support Good Enough to Ship This Today?

Yes. CSS math functions, min(), max(), and clamp(), sit at roughly 94.2 percent global support as of this year's Can I Use data. Every evergreen browser has supported clamp() since 2020, and the gap is almost entirely older browsers your analytics likely show under half a percent of traffic. I stopped writing a fallback font-size declaration above clamp() two years ago and haven't had a bug report trace back to it since. Is there a real reason left to avoid shipping this? I don't think there is.

Why Is clamp() Better Than a Framework-Specific Solution?

Because it isn't a framework feature at all, it's plain CSS, defined in the CSS Values and Units spec, and it works the exact same way whether you're hand-rolling a stylesheet, running Tailwind, or maintaining a component library in whatever framework replaces the current one in three years. No JavaScript resize listener, no ResizeObserver polyfill, no client-side hydration step to get the right font size on first paint. You write the declaration once and the browser's layout engine does the rest, on every device, before your JS bundle finishes parsing.

That's a genuinely underrated property. A media-query-only approach forces you to pick a number of "T-shirt size" breakpoints and hope your content fits neatly between them; clamp() has no such assumption. If you also use a modular type scale to generate your base sizes, apply clamp() to every step, not just headings. Our type scale calculator gives you the min and max values for each step, which is exactly what the two ends of a clamp() need, and the clamp() preview turns those two ends into the finished declaration. Body copy stuck at a fixed 16px while headings scale fluidly is its own kind of visual mismatch, and it's one I still catch in design reviews more often than you'd expect.

Run the numbers yourself

The arithmetic above is easy to follow and easy to get wrong by a decimal place. Put your own sizes in below and the tool hands you the finished declaration, then drag the viewport slider to see where the curve flattens. It also checks the two limits this article cites, the 16px floor and the 2.5x ratio, while you type.

clamp() Live Preview

Enter the sizes you want at each end of your viewport range. The tool runs the slope arithmetic from the section above and hands you the declaration. Then drag the viewport slider and watch where it breaks.

Renders at 40.0px

1200px simulated viewport

Designing the edge cases

The same heading, sized by the declaration below. Drag the slider to the far left and read it as a phone would.

font-size: clamp(1.75rem, 1.333rem + 1.667vw, 3rem);

The preview sets a computed px size, not a live vw value: a real clamp() tracks the browser window, so inside an article column it could only ever show your own viewport. The declaration above is the real one, and it is what you ship.

Open the full clamp() preview for a wider stage, or the type scale calculator to generate the min and max for every step at once.

Frequently Asked Questions

Do I still need media queries if I use clamp()?
For font size, almost never. I still reach for a media query when layout has to change shape, like switching a grid from three columns to one, but type size doesn't need a shape change, it needs a number that scales smoothly. Once you've moved font-size, line-height, and heading scale onto clamp(), the only breakpoints left in a typical stylesheet are structural ones for layout. On the marketing site I mentioned above, replacing six type-only breakpoints with three clamp() declarations removed roughly 40 lines of CSS and, more importantly, removed the six places where a designer had to remember to update every breakpoint in sync whenever a heading size changed.
What's the safest minimum font size for body text?
16px, full stop, for anything a visitor is expected to read for more than a few seconds. I've tested clamp() minimums as low as 14px on a content-heavy blog and watched session recordings where people pinch-zoomed within the first screen. Below 14px you start losing readers with mild low vision even on a good typeface, and 16px is the number most accessibility guides converge on as the practical floor. Set your clamp() minimum to 1rem (16px) for body copy and don't go lower just because a mockup looked tighter on a wide desktop monitor. Headings can dip a little lower relative to their maximum since a large glyph is easier to parse at a slightly reduced size.
Does clamp() work with line-height and spacing too?
Yes, and I'd argue it should. Clamp() accepts any length value, not just font-size, so margin, padding, gap, and even border-radius can scale the same way. The mistake I see most often is teams applying clamp() to font-size alone and leaving line-height as a fixed unitless number. That mostly works because unitless line-height already scales with font-size automatically. Where it breaks down is vertical rhythm: if your heading font size shrinks on mobile but your margin-bottom is a fixed 48px, you get a heading that feels cramped against its own text but strangely distant from the next section. Clamp your spacing tokens alongside your type tokens.
Can I use clamp() with a CSS framework like Tailwind?
Yes, and it doesn't require a plugin. That's actually the point of this whole approach: clamp() is a browser-native CSS function, not a framework feature, so it works identically whether your project uses Tailwind, a component library with CSS-in-JS, or a hand-rolled stylesheet. In Tailwind you can drop a clamp() expression straight into an arbitrary value, something like text-[clamp(1rem,0.9rem+0.5vw,1.25rem)], or define it once as a custom font-size token in your config and reference it by name everywhere. Either way, you're not waiting on a framework update to get fluid type, and the same declaration ports untouched to a totally different stack later.