CSS clamp() for Fluid Typography and Spacing: A Complete Guide
Stepping a heading through three or four fixed sizes at breakpoints is a lot of CSS for a curve that
should just be smooth. clamp() does it in one line: the value scales continuously with the
viewport between two limits, then holds flat outside them — no media-query stack required.
The syntax
font-size: clamp(MIN, PREFERRED, MAX);
clamp() returns the preferred value, but never lets it drop below MIN or
rise above MAX. Make the preferred term include a viewport unit, and the whole expression scales with the
screen between your two endpoints, then stops.
Working out the fluid middle term
The preferred value is a straight line through two points: your minimum size at your smallest target viewport, and your maximum size at your largest. Say you want body text to grow from 16px at a 320px viewport to 24px at a 1280px viewport. The slope (size change per pixel of viewport width) is:
slope = (24 - 16) / (1280 - 320) = 8 / 960 ≈ 0.008333
Expressed as a vw value (since 1vw = 1% of viewport width, or viewport-px / 100), that slope becomes
0.8333vw. The constant term is whatever's left over once you subtract the slope's contribution
at the starting point:
constant = 16 - (0.008333 * 320) = 16 - 2.6667 ≈ 13.333px ≈ 0.8333rem
Put together, the full declaration is:
font-size: clamp(1rem, 0.8333rem + 0.8333vw, 1.5rem);
At a 320px viewport this resolves to exactly 16px; at 1280px, exactly 24px; and at any width in between, a smooth linear step. Below 320px it holds flat at the 1rem minimum, and above 1280px it holds flat at the 1.5rem maximum. Working this arithmetic out by hand for every heading level gets tedious fast — the CSS clamp() generator takes the same two size/viewport pairs and returns the ready-to-paste value directly.
A full fluid type scale
| Role | Min (320px) | Max (1280px) | clamp() value |
|---|---|---|---|
| Body | 16px | 18px | clamp(1rem, 0.9375rem + 0.2083vw, 1.125rem) |
| H3 | 20px | 24px | clamp(1.25rem, 1.1667rem + 0.3333vw, 1.5rem) |
| H2 | 28px | 36px | clamp(1.75rem, 1.5833rem + 0.8333vw, 2.25rem) |
| H1 | 36px | 56px | clamp(2.25rem, 1.5833rem + 2.0833vw, 3.5rem) |
It's not just for font-size
The same technique works anywhere a measurement should scale smoothly rather than jump at a breakpoint — section padding, grid gaps, a max-width container, or vertical rhythm between elements:
.section {
padding-block: clamp(2rem, 1.5rem + 2vw, 5rem);
}
A clamped padding value like this removes a whole tier of "medium screen" media queries that would otherwise exist purely to nudge spacing between the mobile and desktop values.
Common mistakes
- Using px instead of rem for the min/max/constant terms. A pure-px clamp ignores a user's browser font-size preference; keeping the fixed terms in rem (with only the scaling term in vw) keeps the value responsive to accessibility zoom settings too.
- Skipping the min or max. A bare
font-size: 4vwwith no clamp keeps growing indefinitely on an ultra-wide monitor and keeps shrinking indefinitely on a tiny viewport — the clamp's whole job is to put a floor and a ceiling on that. - Picking endpoints that were never checked on a real screen. The formula is only as good as the two size/viewport pairs you feed it — cross-check the chosen minimum against your smallest supported device width, not just a round number.
- Forgetting clamp() also works for anything else measurable — width, gap, and margin all accept the same pattern; it isn't font-size-specific.
Clamp() for more than type: icon and button sizing
The same pattern scales a component's dimensions, not just its text — a hero icon that should read as noticeably larger on desktop without being oversized on a small phone:
.hero-icon {
width: clamp(2rem, 1.5rem + 2vw, 4rem);
height: auto;
}
This removes the need for a separate breakpoint-specific override just to bump one icon's size up a tier on larger screens.
Why not just use vw alone?
A bare font-size: 2vw has no floor or ceiling, so it keeps shrinking toward zero on a tiny
viewport and keeps growing without limit on an ultra-wide monitor — exactly the failure mode
clamp()'s min and max terms exist to prevent. Treat the viewport-unit term as the "shape" of
the scaling curve, and the min/max as the safety rails that make it usable at the extremes.
Testing a clamp value across real breakpoints
Because the formula is only as good as the two size/viewport pairs fed into it, it's worth actually resizing a real browser window (or using devtools' responsive mode) across the full range from your chosen minimum to maximum viewport, watching for the moment text visually feels too small or too large — not just trusting the two endpoint numbers in isolation. A clamp value that looks right at 320px and 1280px can still have an awkward middle if the two endpoints were chosen without checking anything in between.
Choosing sensible min/max endpoints in the first place
The formula itself is neutral about whether your chosen endpoints are actually good design decisions — it will happily compute a technically correct fluid value between a minimum that's too small to read comfortably and a maximum that overflows a headline's container on a very wide screen. Before feeding two numbers into the formula, it's worth confirming both endpoints independently: does the minimum stay legible against your smallest supported viewport, and does the maximum still fit its container (and look proportionate next to surrounding elements) at your largest supported viewport. The math handles the curve between them; it doesn't validate the two ends are actually reasonable.
Browser support
clamp() has shipped in every major browser engine since 2020 and is safe to use without a
fallback in virtually any current project.
Clamp() vs. container queries for component-local scaling
clamp() scales against the viewport, which is the right tool for page-level type and
spacing — but a component that needs to scale based on its own container's width, independent of where it's
placed on the page, is better served by container query units (cqw) inside a similar clamp
expression, or a container query breakpoint entirely. The two techniques solve related but distinct
problems: one tracks the whole page, the other tracks a specific box.
Frequently Asked Questions
Why use rem inside the clamp instead of px?
Rem keeps the fixed min/max/constant terms responsive to a user's browser font-size setting, which px ignores — better for accessibility. The scaling term stays in vw regardless, since that's what ties it to viewport width.
Can clamp() reference a CSS custom property?
Yes — clamp(var(--font-min), var(--font-fluid), var(--font-max)) works, which is useful for
centralizing a type scale as design tokens rather than repeating the raw expression everywhere.
Does clamp() cause any performance cost versus a fixed font-size?
No — it's resolved at layout time the same as any other CSS length; there's no runtime or JavaScript cost involved.
What happens if MIN is larger than MAX?
The spec says the result is the same as if MIN and MAX were equal to MIN — effectively a fixed value — so it's worth double-checking the two endpoints aren't accidentally reversed.
Is clamp() the same as min() and max() combined?
Effectively yes — clamp(MIN, VAL, MAX) is shorthand for max(MIN, min(VAL, MAX)),
and all three functions are useful independently for narrower cases (just a ceiling, or just a floor)
where a full three-argument clamp isn't needed.
Can I nest calc() inside a clamp() argument?
Yes — each of the three arguments accepts a full calc() expression, which is useful for
combining a viewport-relative term with a fixed offset beyond the basic slope/intercept pattern shown
above.