Debugging clamp(): Values That Are Ignored, Stuck or Never Fluid
clamp() looks simple — a minimum, a preferred value, a maximum — and most of the time it behaves. When it does not, it fails quietly: the declaration is dropped, or the value sits at one bound at every width, or the "fluid" size turns out to change by half a pixel across the whole range of real devices. None of these produce an error in the console. The narrow problem this page solves is diagnosing those failures quickly, with a mental model of how clamp() resolves, a checklist of the classic mistakes, and a DevTools workflow for watching the value change. It belongs to Fluid Typography With clamp() in the Mastering Container Queries & Responsive Layouts guide.
How clamp() actually resolves
clamp(MIN, VAL, MAX) is defined as max(MIN, min(VAL, MAX)). Reading it in that order explains most surprises. First the preferred value is capped at the maximum; then the result is raised to at least the minimum. The minimum is applied last, so it always wins a conflict.
Each of the three arguments is a calculation in its own right and is resolved at computed-value time against the element's context: vw against the viewport, cqi against the container, em against the element's font size, rem against the root, and percentages against whatever that property's percentage basis is. The three results must all be of the same type — all lengths, all numbers, all percentages that resolve to lengths — or the expression is invalid.
The graph is the most useful debugging tool on this page. A clamp() value is fluid only between two crossover widths: where the preferred value equals the minimum, and where it equals the maximum. Outside that stretch it is a constant. Every "my clamp does not work" report is a question about where those crossovers are, or whether the expression is valid at all.
The complete implementation: five broken declarations
Each rule below contains one classic mistake, with the diagnosis and fix beside it.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Broken clamp() values</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 1.5rem; }
/* 1. MIN larger than MAX. Valid, but always 3rem: the minimum is
applied last. FIX: swap the bounds. */
.a { font-size: clamp(3rem, 5vw, 2rem); }
.a-fixed { font-size: clamp(2rem, 5vw, 3rem); }
/* 2. Mixed types. A length and a unitless number cannot be compared,
so the whole declaration is INVALID and dropped. FIX: give every
argument the same type. */
.b { padding: clamp(1rem, 2vw, 3); }
.b-fixed { padding: clamp(1rem, 2vw, 3rem); }
/* 3. Crossovers outside real screens. 1rem + 0.1vw only reaches 1.2rem
at a 3200px viewport, so on every real screen the value sits near
the minimum. FIX: steepen the slope so the crossovers land between
roughly 360px and 1280px. */
.c { font-size: clamp(1rem, 1rem + 0.1vw, 1.2rem); }
.c-fixed { font-size: clamp(1rem, 0.9rem + 0.4vw, 1.2rem); }
/* 4. A custom property that is invalid at computed-value time. --gap is
"large", not a length, so clamp() becomes invalid when computed and
the property falls back to its INITIAL value (0 for gap), not the
previous declaration. FIX: store lengths in the token. */
.d { --gap: large; gap: clamp(0.5rem, var(--gap), 2rem); }
.d-fixed { --gap: 3vw; gap: clamp(0.5rem, var(--gap), 2rem); }
/* 5. Dividing lengths. (100vw - 20rem) / 60rem is length ÷ length,
which calc() rejects. FIX: multiply by a number instead, or use
tan(atan2()) to obtain a ratio. */
.e { font-size: clamp(1rem, 1rem + (100vw - 20rem) / 60rem * 1rem, 2rem); }
.e-fixed { font-size: clamp(1rem, 0.667rem + 1.667vw, 2rem); }
</style>
</head>
<body>
<p class="a">Always 3rem</p>
<p class="a-fixed">Fluid 2–3rem</p>
</body>
</html>
Case 4 deserves attention because its failure mode differs from the others. A declaration that is invalid at parse time — cases 2 and 5 — is simply discarded, so an earlier declaration for the same property still applies. A declaration that only becomes invalid at computed-value time — because a var() substituted something unusable — is not discarded; it computes to the property's initial value or, for inherited properties, the inherited value. A fallback declaration written earlier in the rule does not rescue it. That is why a bad token can reset gap to zero even though the stylesheet looks like it has a sensible value.
The key technique: find the crossover widths
When a value is valid but "not fluid", compute the two widths where the preferred value meets each bound. For a preferred value of the form A + B·vw, solve A + B·(w/100) = MIN and A + B·(w/100) = MAX for w. With clamp(1rem, 0.9rem + 0.4vw, 1.2rem) at a 16-pixel root:
The demo makes the crossovers visible. Both bars use clamp() for their width; the solid one has a steep preferred value and visibly moves through all three phases as the frame is resized, while the dashed one has a preferred value so shallow that it barely leaves its minimum at any realistic width.
If both crossovers fall below the smallest screen you support, the value is effectively fixed at the maximum; if both fall above the largest, it is fixed at the minimum. The broken case 3 above crosses the minimum at 0 pixels and the maximum at 3200 pixels, so real screens only ever see the bottom tenth of its range. Designing values from chosen crossover widths, rather than guessing coefficients, avoids the problem entirely; the method is laid out in Fluid Typography Without JavaScript.
Zoom changes the crossovers
One more subtlety: when the user zooms the page, CSS pixels grow, so the viewport shrinks in CSS pixels while rem values stay the same number of CSS pixels. A preferred value with a large vw component therefore decreases relative to the text as zoom increases, and the value may drop back to its minimum at high zoom. That is the mechanism behind the WCAG 1.4.4 failures described in Fluid Type, Accessibility and Zoom. When debugging a value that "stops growing" for some users, check it at 200% zoom as well as at different window sizes.
DevTools workflow
- Read the computed value. Select the element; the Computed pane shows the final pixel value of
font-size,paddingor whatever property usesclamp(). If it shows a value unrelated to the declaration, the declaration was invalid. - Check for a strikethrough or warning. Chromium marks declarations that failed to parse with a warning icon and strikes them through; Firefox shows invalid values with a warning triangle. A computed-time failure is not struck through, so compare the computed value with what you expected.
- Inspect custom properties. Hover a
var()in the Styles pane to see the substituted value. If it is not a length where one is required, that is case 4. - Sweep the width. Use responsive design mode and drag the viewport from narrow to wide while watching the Computed value. It should be flat, then rising, then flat. If it never moves, recompute the crossovers.
- Test zoom. Repeat the sweep at 200% zoom to confirm the value still responds.
Browser support
clamp(), min() and max() are supported in Chrome and Edge 79+, Firefox 75+ and Safari 13.1+. The trigonometric functions used for the tan(atan2()) ratio idiom are supported in current versions of all three engines. Container query units, often used as the preferred value's variable term, are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+. The behaviour described for invalid-at-computed-value-time declarations is defined by the Custom Properties specification and consistent across current engines.
FAQ
What happens if the minimum in clamp() is larger than the maximum?
The minimum wins. clamp(MIN, VAL, MAX) is defined as max(MIN, min(VAL, MAX)), so when MIN exceeds MAX the result is always MIN. The declaration is valid and silently produces a constant value instead of a fluid one.
Why is my clamp() declaration ignored entirely?
Usually a type mismatch: mixing a length with a plain number, such as clamp(1rem, 2vw, 3), or dividing a length by a length. Such expressions are invalid at parse time and the whole declaration is dropped, leaving the inherited or previous value.
Why does my fluid font size never change? Either the preferred value never leaves the range between the bounds at any real viewport width, or the bounds are so close together that the change is invisible. Calculate the viewport widths at which the preferred value equals each bound; if both fall outside real screen sizes, the value is effectively constant.
How do I see what a clamp() value resolves to?
Select the element and read the Computed pane in DevTools, which shows the final pixel value. Resize the window or container while watching it. Chromium also lets you hover a var() reference in the Styles pane to see the custom property's value.
Related
- Fluid Typography With clamp() — the parent guide.
- clamp() vs Media Query Typography — when a stepped scale is simpler.
- Registered Properties and Type Safety — preventing invalid tokens with @property.
- Fluid Space Scale With clamp() — the same function applied to spacing.
Related articles
More pages in the same section.