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 shape of a clamp() value The output is constant at the minimum up to the first crossover width, rises along the preferred value's line between the two crossovers, and is constant at the maximum after the second crossover. If both crossovers fall outside the range of real screen widths, the value never visibly changes. clamp(MIN, VAL, MAX) across widths viewport or container width VAL alone MIN MAX VAL = MIN VAL = MAX The fluid range is only the stretch between the two crossover widths.

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:

Crossover arithmetic for one clamp() value The preferred value equals the 16 pixel minimum when 14.4 plus 0.004 times the width equals 16, at a width of 400 pixels. It equals the 19.2 pixel maximum at a width of 1200 pixels. Between 400 and 1200 pixels the size is fluid. clamp(1rem, 0.9rem + 0.4vw, 1.2rem) VAL(w) = 14.4px + 0.004 × w 14.4 + 0.004w = 16 w = 400px 14.4 + 0.004w = 19.2 w = 1200px Fluid from 400px to 1200px: a sensible range for real screens.

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.

Live demoWatching clamp() stay flat, rise, then stay flat
Drag the frame from narrow to wide: each bar is pinned at its minimum, then grows with the frame, then stops at its maximum. The broken bar barely moves. Drag the bottom-right corner to resize the frame in either direction.

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

  1. Read the computed value. Select the element; the Computed pane shows the final pixel value of font-size, padding or whatever property uses clamp(). If it shows a value unrelated to the declaration, the declaration was invalid.
  2. 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.
  3. 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.
  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.
  5. 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 articles

More pages in the same section.