Skip to main content

Harshal V. LADHE

min(), max(), and clamp() — CSS's Value-Bounding Functions

Three comparison functions that pick a value from a rule, without a single media query.
Published at:
Last updated:
Estimated reading time:4 min read

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>

Read-only
Ln , Col HTML2.2 KBUTF-8

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 @media override
  • Replacing a base rule + one-or-two @media overrides 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 for rem), 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. If VAL is written smaller than MIN, the result is just MIN at every width — CSS won't warn about it, and it silently behaves like a plain fixed value. Sanity-check the floor/ceiling relationship when a clamp() 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 some transform and 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.

Categories:CSS
Tags:

Changelog

  • Initial publication.
This snippet is licensed under CC BY 4.0 by the author.

Share this snippet