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:5 min read
min(), max(), and clamp() CSS infographic showing value comparison, responsive sizing, safe-area insets, and CSS code without media queries.(opens full-size image in a new tab)
Values bounded by min(), max() and clamp(), with no media queries needed.

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));
}

When the same formula keeps reappearing with different numbers, you can name it once with a custom @function and call it like a built-in, such as --fluid(1rem, 2rem).

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.

Frequently Asked Questions

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

  • — Added Frequently Asked Questions section
  • — Added cross-reference to tutorial 'CSS Custom Functions with `@function`: Reusable Logic Without Sass'
  • — Initial publication.
This tutorial is licensed under CC BY 4.0 by the author.

Share this tutorial

Spotted a mistake or something outdated in this tutorial? Suggest a correction.

  • Fluid Typography and Spacing with clamp()

    One clamp() declaration scales type, padding, and gaps smoothly — no breakpoint ladder.
    clamp(min, preferred, max) — with a preferred value built from rem plus a viewport or container-relative unit — scales font-size, padding, and gaps continuously between a floor and ceiling instead of jumping at fixed breakpoints, replacing a media-query ladder with one line per property.
    Published at:
  • Dynamic Viewport Height Units: dvh, svh, and lvh

    Tracks the mobile address bar's actual visible height live.
    100vh has always meant the LARGEST possible viewport on mobile, so content sits behind the address bar until it's scrolled away. svh assumes the smallest viewport, lvh assumes the largest, and dvh is the only one of the three that tracks the browser chrome's actual current state, live.
    Published at:
  • Dynamic Viewport Width Units: dvw, svw, and lvw

    Width-axis siblings of dvh/svh/lvh, useful for full-bleed breakouts.
    dvw, svw, and lvw mirror dvh/svh/lvh onto the width axis, but the browser chrome that makes dvh diverge from vh is a vertical UI element — so on virtually every current browser, all four width units render identically. Where they actually earn their place is different: full-bleed breakouts and edge-to-edge mobile carousels, not a live-resizing address bar.
    Published at: