min(), max(), and clamp() — CSS's Value-Bounding Functions
Introduction
A surprising amount of CSS boils down to "use this value, but never let it go below/above that other value" — a paragraph that shouldn't get wider than ~60 characters no matter how wide the screen is, padding that shouldn't collapse to nothing on a phone, a card that should track a percentage of its container but not blow past a sane maximum. The traditional tool for all three is the same: a base rule, then a @media override (or three) that swaps in a different fixed value past some width.
min(), max(), and clamp() are CSS's comparison functions — they pick a value out of a list of expressions, evaluated live, wherever a length/percentage/number is expected. No media query, no JavaScript measuring anything.
The Three Functions
min(A, B, ...) /* resolves to the SMALLEST of its arguments */
max(A, B, ...) /* resolves to the LARGEST of its arguments */
clamp(MIN, VAL, MAX) /* resolves to VAL, bounded between MIN and MAX */Each accepts a comma-separated list of expressions — lengths, percentages, or anything calc() accepts — and picks one:
.measure {
width: min(90%, 60ch); /* whichever is SMALLER */
}
.safe-pad {
padding-inline: max(1rem, 4%); /* whichever is LARGER */
}
.card {
width: clamp(200px, 50%, 500px); /* 50%, bounded 200-500px */
}clamp() isn't a fourth, unrelated idea — it's shorthand for a min()/max() pair: clamp(MIN, VAL, MAX) is exactly equivalent to max(MIN, min(VAL, MAX)). Writing it as clamp() is just more direct once the pattern is "bound a value on both sides."
Try It Live
Resize the frame below and watch all three patterns resolve independently — the readout shows each one's resolved pixel value at the current width.
min() / max() / clamp() Playground
Resize the frame and watch three common patterns resolve differently, live: a min(90%, 60ch) readable measure, a max(1rem, 4%) safe inset, and a clamp(200px, 50%, 500px) bounded card width.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>min() / max() / clamp() Playground</title> </head> <body> <main class="playground"> <header class="playground-header"> <h1>min() / max() / clamp() Playground</h1> <p>Resize the frame below and watch three common patterns resolve differently — a readable measure, a safe-area-aware inset, and a bounded card width.</p> </header> <div class="field" id="width-field"> <label for="width-input">Simulated container width: <span id="width-label">640px</span></label> <div class="range-row"> <span class="range-edge">240px</span> <div class="range-track"> <input type="range" id="width-input" min="240" max="900" step="1" value="640"> <output class="range-bubble" id="width-value" for="width-input">640px</output> </div> <span class="range-edge">900px</span> </div> </div> <section class="demo-area" aria-label="min, max, and clamp width demos"> <div class="frame" id="frame"> <div class="demo-row"> <div class="demo-item"> <p class="demo-label"><code>width: min(90%, 60ch)</code> — readable measure</p> <div class="box box-min" id="box-min">Never wider than 60 characters, but still shrinks below that on narrow containers.</div> </div> <div class="demo-item"> <p class="demo-label"><code>padding-inline: max(1rem, 4%)</code> — safe inset</p> <div class="box box-max" id="box-max">Padding never drops below 1rem, but grows past it on wide containers.</div> </div> <div class="demo-item"> <p class="demo-label"><code>width: clamp(200px, 50%, 500px)</code> — bounded card</p> <div class="box box-clamp" id="box-clamp">Tracks 50% of the frame, but never below 200px or above 500px.</div> </div> </div> </div> </section> <output id="readout" class="readout" aria-live="polite"></output> </main> <script src="./index.js"></script> </body> </html>
Starting sandbox…
No console output yet.
Pattern: A Readable Measure
Long lines of text are hard to read — the usual guidance is capping a paragraph around 45-75 characters wide. ch units approximate character width, so min() gives a width that's the smaller of "90% of the container" or "60 characters," without ever needing to know the container's actual size:
p {
width: min(90%, 60ch);
/* Narrow container -> shrinks with it (the 90% branch wins) */
/* Wide container -> stops at 60ch (the 60ch branch wins) */
}Pattern: A Safe-Area-Aware Inset
max() is the natural fit for "never below this floor" — most commonly paired with env(safe-area-inset-*) so content clears a device's notch/rounded corners without adding excess padding on devices that don't have one:
.page {
padding-inline: max(1rem, env(safe-area-inset-left));
/* Normal device -> env() resolves to 0, so 1rem wins */
/* Notched device -> env() may exceed 1rem, so it wins instead */
}Pattern: A Bounded, Proportional Size
clamp() is the right tool the moment a value should track something proportional (a percentage, a viewport unit) but never go past a sane floor or ceiling — this is the same mechanism behind fluid typography and spacing, just applied to a plain width here instead of a font-size:
.card {
width: clamp(200px, 50%, 500px);
/* Narrow container -> 50% would be under 200px, so 200px wins */
/* Wide container -> 50% would exceed 500px, so 500px wins */
/* Everything between -> tracks 50% exactly */
}Nesting and Mixing Units
All three compose with calc() and with each other, and their arguments don't need to share a unit — the browser resolves each expression to a common unit before comparing:
.el {
/* clamp() nested inside calc(), plus mixed rem/vw/px units in one call */
margin-inline: calc(clamp(1rem, 4vw, 3rem) / 2);
/* max() of two totally different kinds of expression */
inset-block-start: max(2rem, calc(env(safe-area-inset-top) + 0.5rem));
}Where Should You Use This?
- Readable measures:
width: min(90%, NNch)for any block of body text - Safe-area/notch-aware insets:
max(base-value, env(safe-area-inset-*)) - Any width/size that should be proportional but bounded, without a
@mediaoverride - Replacing a base rule + one-or-two
@mediaoverrides that only ever changed a single fixed value
Production Considerations & Edge Cases
- Arguments are compared as resolved values, not as written.
min(50%, 20rem)doesn't compare "50" to "20" — the browser resolves both to actual lengths first (against whatever%is relative to, at the current font-size forrem), then picks the smaller resolved value. This is why mixing unit types in one call is safe and common. clamp()'s middle argument isn't required to be "between" the other two on paper. IfVALis written smaller thanMIN, the result is justMINat every width — CSS won't warn about it, and it silently behaves like a plain fixed value. Sanity-check the floor/ceiling relationship when aclamp()value looks like it's "stuck."- These aren't limited to sizing properties. Anywhere a
<length>,<percentage>, or plain<number>is valid —calc()expressions, custom properties, even sometransformand animation values —min()/max()/clamp()work the same way. - Browser support is universal at this point for all three (Chrome/Edge 79+, Firefox 75+, Safari 13.1+) — there's no fallback consideration left to make; this has been broadly safe for years.
Key Takeaway
min(), max(), and clamp() replace "a base value plus a @media override that only ever swaps in one different number" with a single declaration that picks the right value itself, live, at every width. clamp() in particular is worth reaching for by default whenever a size needs to be proportional and bounded — see fluid typography and spacing for what that looks like applied to an entire type scale.
Changelog
- — Initial publication.