clamp() is linear interpolation in disguise
The CSS Values and Units Module Level 4 specification defines `clamp(MIN, VAL, MAX)` as exactly equivalent to `max(MIN, min(VAL, MAX))`. Read it right to left: the `min(VAL, MAX)` keeps the value from exceeding the ceiling; the `max(MIN, ...)` keeps it from dropping below the floor. In the middle sits `VAL`, the "preferred value", and that is where the fluid behavior lives. If `VAL` were a constant, `clamp()` would be pointless; the curve only comes alive because `VAL` usually depends on screen width, through viewport units.
- MIN (floor)
- The smallest allowed value. On narrow screens, it wins.
- VAL (preferred)
- The fluid expression, the line that interpolates between the bounds. This is where `vw` comes in.
- MAX (ceiling)
- The largest allowed value. On wide screens, it wins.
Note that `clamp()` does no smoothing, curve or easing: between floor and ceiling the growth is strictly linear. If you want an accelerating progression, text that grows slowly and then takes off, `clamp()` alone will not give it; that is the territory of timing curves, which you shape in the cubic-bezier editor. For responsive type, though, the straight line is exactly what you want: predictable and easy to derive.
Deriving the formula from scratch
The problem: I want a minimum size `y1` at a width `x1` and a maximum size `y2` at a larger width `x2`, with a straight line connecting the two points. This is high-school geometry. The slope is the vertical change divided by the horizontal one; the intercept is where the line crosses the axis at `x = 0`. The only CSS subtlety is translating that line into units the browser understands: the part proportional to width becomes `vw` (1vw = 1% of the window width), and the fixed part becomes `rem`.
m = (y₂ − y₁) / (x₂ − x₁) [inclinação / slope]
b = y₁ − m · x₁ [intersecção em px / intercept in px]
font-size = clamp( y₁ , b_rem + (100·m)vw , y₂ )- y₁
- minimum font size (the clamp floor)
- y₂
- maximum font size (the clamp ceiling)
- x₁
- viewport width where the font should sit at y₁
- x₂
- viewport width where the font should reach y₂
- m
- slope of the line, in px of font per px of width
- b
- intercept of the line at x = 0, in px
- b_rem
- the intercept b converted from px to rem (b ÷ root size)
- Pick the four pointsSet `y1` and `x1` (the minimum pair) and `y2` and `x2` (the maximum pair). Do the arithmetic entirely in px.
- Compute the slope`m = (y2 − y1) / (x2 − x1)`. This number is tiny, thousandths, because it is px of font per px of width.
- Compute the intercept`b = y1 − m · x1`. It is the value of the line at `x = 0`; it can be positive or negative.
- Translate to CSSPreferred = `b_rem + (100·m)vw`. Assemble `clamp(y1, preferred, y2)` with y1 and y2 in rem.
Two examples with the numbers
Example 1, body text. I want 1rem (16px, with a 16px root) at a 320px window, rising to 2rem (32px) at a 1280px window. So `y1 = 16`, `x1 = 320`, `y2 = 32`, `x2 = 1280`. The slope is `m = (32 − 16) / (1280 − 320) = 16 / 960 = 0.016667`, which in vw is `100 × 0.016667 = 1.6667vw`. The intercept is `b = 16 − 0.016667 × 320 = 16 − 5.3333 = 10.6667px`, which in rem is `10.6667 ÷ 16 = 0.6667rem`. Result: `clamp(1rem, 0.6667rem + 1.6667vw, 2rem)`.
Check it: at a 320px window, `1.6667vw = 5.3333px`, and adding `0.6667rem = 10.6667px` gives exactly 16px = 1rem. At a 1280px window, `1.6667vw = 21.333px` plus 10.6667px gives 32px = 2rem. The line passes through the two points we asked for, and `clamp()` freezes at the bounds outside the interval.
Example 2, a heading. I want 1.5rem (24px) at a 360px window and 3rem (48px) at a 1440px window. So `m = (48 − 24) / (1440 − 360) = 24 / 1080 = 0.022222`, that is `2.2222vw`. The intercept is `b = 24 − 0.022222 × 360 = 24 − 8 = 16px`, which in rem is exactly `16 ÷ 16 = 1rem`, a case where the numbers come out round. Result: `clamp(1.5rem, 1rem + 2.2222vw, 3rem)`. At 360px: `2.2222vw = 8px` plus 1rem (16px) = 24px = 1.5rem; at 1440px: `2.2222vw = 32px` plus 16px = 48px = 3rem.
:root {
/* Example 1, body text: 16px (1rem) @ 320px -> 32px (2rem) @ 1280px
m = (32-16)/(1280-320) = 1/60 px/px -> 100m = 1.6667vw
b = 16 - (1/60)*320 = 10.6667px -> 10.6667/16 = 0.6667rem */
--fs-body: clamp(1rem, 0.6667rem + 1.6667vw, 2rem);
/* Example 2, heading: 24px (1.5rem) @ 360px -> 48px (3rem) @ 1440px
m = (48-24)/(1440-360) = 1/45 px/px -> 100m = 2.2222vw
b = 24 - (1/45)*360 = 16px -> 16/16 = 1rem */
--fs-h1: clamp(1.5rem, 1rem + 2.2222vw, 3rem);
}
body { font-size: var(--fs-body); }
h1 { font-size: var(--fs-h1); }The curve: plateau, ramp, plateau
Plotting font size against width makes what `clamp()` does obvious: a plateau at the minimum while the screen is narrow, a linear ramp across the chosen interval and a plateau at the maximum once the screen gets too wide. The chart below compares two clamps. The conservative one is Example 1 (16px→32px, between 320px and 1280px). The aggressive one is `clamp(1.5rem, 0.1207rem + 6.8966vw, 4rem)` (24px→64px, between 320px and 900px): a much steeper ramp that saturates early. Note both are straight lines between two knees, no smoothing.
View the data
| x | Conservative (16→32px) | Aggressive (24→64px) |
|---|---|---|
| 280 | 16px | 24px |
| 320 | 16px | 24px |
| 480 | 18.7px | 35px |
| 640 | 21.3px | 46.1px |
| 800 | 24px | 57.1px |
| 960 | 26.7px | 64px |
| 1,280 | 32px | 64px |
| 1,600 | 32px | 64px |
An over-aggressive clamp has two problems: the font changes too fast between two nearby screen sizes (jarring when resizing) and it saturates early, wasting the range of larger widths. A conservative clamp breathes better. Rather than memorizing the numbers, generate your own and watch the curve in the tool below.
The WCAG 1.4.4 trap
Here is the part the "text that grows" tutorials tend to skip. WCAG Success Criterion 1.4.4 (Resize Text), level AA, requires that text can be resized up to 200% without loss of content or functionality, without assistive technology, using only the browser's own controls. And a `font-size` in pure `vw` fails exactly that test. Viewport units are anchored to the window size, not to the user's font preference; when someone zooms text or raises the browser's base font size, the `vw` does not budge. The user hits "increase" and nothing happens.
There is a second important safeguard: do not set a ceiling (`MAX`) that is too low. If your `clamp()` maximum is below twice the base size, you prevent the user from reaching the 200% the standard asks for, even with the rem term. A safe rule of thumb: only set a maximum if it is at least twice the minimum, or accept having no maximum at all. And test for real, the criterion demands manual verification, resizing the text and confirming it actually grows.
- 2016The "CSS Locks" are born
Mike Riethmuller popularizes the linear-interpolation formula with calc() and vw; Tim Brown dubs it CSS Locks. It is the same line clamp() bakes in today.
- 2019Roselli documents the zoom failure
Adrian Roselli publishes "Responsive Type and Zoom," showing with tests that vw-anchored text stops the user from enlarging the font.
- 2020clamp() reaches browsers
clamp(), min() and max() gain cross-browser support (Chrome 79, Firefox 75, Safari 13.1), turning the CSS lock into a native one-line function.
- 2022svw / lvw / dvw units
The small, large and dynamic viewport units reach browsers (Safari 15.4, Firefox 101, Chrome 108) to handle the mobile address bar.
About those new units: `vw` equals `lvw` (the large viewport, with the address bar retracted). `svw` measures the small viewport (bar visible) and `dvw` tracks the change in real time. They exist because, on mobile, the address bar appears and disappears as you scroll, changing the usable width. For `font-size` this rarely matters, the horizontal difference is tiny, and `dvw` can even cause reflow while scrolling; the caution around these units weighs more on heights (`dvh`) and full-screen elements.
Beyond typography (and the review)
The same `clamp()` works for any property that takes a length: internal spacing, grid gaps and column widths that breathe with the screen without a stack of media queries. But there are details that only surface once you leave `font-size`. The accordion below gathers the three that cause the most bugs.
clamp() in padding, gap and column width
For spacing, the pattern is `padding: clamp(1rem, 5vw, 3rem)`, margins that grow with the screen but never touch the edges nor explode. For content widths, `width: clamp(45ch, 60vw, 75ch)` keeps the text line in a readable band (`ch` is the width of the "0" glyph). Here the rem component is optional: spacing and width are not subject to criterion 1.4.4, which is about text. Even so, mixing in a font-relative unit (rem/em/ch) tends to give more predictable results.
Why min() and max() alone rarely suffice
`min(a, b)` returns the smaller value and `max(a, b)` the larger one. A `width: min(90vw, 60rem)` caps the width at 60rem on large screens, with no floor; a `max()` guarantees a minimum, with no ceiling. What almost every responsive layout wants, though, is both at once: a floor and a ceiling with a fluid preference in the middle. Doing that with nested min() and max() means writing `max(MIN, min(VAL, MAX))`, which is literally the definition of clamp(). So: you can, but clamp() is the readable shortcut for the common case.
The trap of swapping MIN and MAX
If you swap the bounds, `clamp(MAX, VAL, MIN)` with the larger one first, CSS will not fix it for you. The spec resolves `max(MIN, min(VAL, MAX))` literally: when `MIN > MAX`, the inner `min()` returns MAX and the outer `max()` returns MIN, so the result locks to MIN and ignores everything. The text sticks at one size and you swear the clamp "does not work." Rule: the first argument is always the smallest, the third always the largest.
Before shipping, run through the review below. It covers the mistakes that do not show up on your 1440px monitor but blow up on someone's phone or under the zoom of a person who needs it.
- The preferred value includes a rem term (the intercept b_rem), never pure vw on font-size.
- I tested at 200% zoom and the text actually grew (WCAG 1.4.4).
- I tested at a 320px-wide window and the font neither shrank to nothing nor broke the layout.
- The maximum (MAX) is at least twice the minimum, or there is no maximum, so zoom is not capped before 200%.
- MIN is the first argument and MAX the last; I checked I did not swap them.
- I did not drop a raw clamp() into line-height without care: unitless line-height scales with the font; a clamp in px/rem freezes the line spacing.
Frequently asked questions
How does clamp() work in CSS?
Why should I not use only vw for font-size?
How do I calculate the values of a clamp() for font size?
What is the difference between vw, svw, lvw and dvw?
Can I use clamp() for padding and width, not just font size?
`clamp()` is a straight line with two brakes: derive the slope and intercept from your minimum and maximum pairs, and never write the preferred value in vw alone. The rem term is not decoration, it is what keeps the text obedient to zoom and to WCAG 1.4.4. Do the math once, test at 200% and at 320px, and you have fluid typography that is also accessible.