TechEarl

CSS calc(): Mixing Units, with min(), max(), and clamp()

How CSS calc() mixes units like 100% - 2rem, why the spaces around + and - are mandatory, and how min(), max(), and clamp() give you caps, floors, and fluid type with no media queries.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
How CSS calc() mixes units, why the spaces around plus and minus are required, and how min(), max(), and clamp() handle caps, floors, and fluid typography.

calc() lets you do arithmetic in a CSS value, and its one genuine superpower is mixing units the engine resolves at different times: percentages (resolved against the parent), viewport units (resolved against the viewport), and absolute lengths like rem and px. You cannot express "the full width minus a 2rem gutter" any other way, because at authoring time you do not know what 100% will be in pixels. calc() defers that arithmetic to layout:

css
.content {
  /* full parent width, minus a fixed gutter on each side */
  width: calc(100% - 4rem);
}

That single capability is why calc() has been a daily tool since long before its modern siblings arrived. The rest of this page is the detail: the whitespace rule that breaks more calc() than anything else, nesting and custom properties, and then min(), max(), and clamp(), which are calc()'s newer relatives for caps, floors, and fluid sizing without a single media query. All four are Baseline and safe to use in production today.

The one rule that breaks calc(): spaces around + and -

This is the bug everyone hits at least once. The + and - operators must have whitespace on both sides. It is not a style preference, it is in the spec, and it exists to remove an ambiguity:

css
/* BROKEN: parsed as "100% followed by a negative length", which is invalid */
width: calc(100% -2rem);

/* CORRECT: 100% minus 2rem */
width: calc(100% - 2rem);

Without the space, -2rem reads as a single negative number sitting next to a percentage, which is not a valid expression, so the whole declaration is thrown out and your element silently keeps its old width. The * and / operators do not require the spaces (calc(100%/3) is fine), but I add them everywhere anyway so every operator looks the same:

css
width: calc(100% / 3);   /* division: spaces optional, but tidy */
margin: calc(1rem * 2);  /* multiplication: same */

Historically there was a second trap: old CSS minifiers stripped the spaces around - and quietly broke calc() in the built file even when the source was correct. Modern minifiers (cssnano, esbuild, Lightning CSS) all know the rule now, so this is mostly a museum piece, but if a calc() works in dev and dies in the production bundle, an over-eager minifier is the first thing I check.

Nesting and dividing by a unitless number

calc() nests, and you can divide a length by a plain number (no unit) to split space:

css
.col {
  /* three columns with two 1rem gaps between them */
  width: calc((100% - 2rem) / 3);
}

The inner calc() is implicit here. You can also write an explicit nested calc() when it reads more clearly, and the result is identical:

css
width: calc(calc(100% - 2rem) / 3);

One thing the math does not let you do is multiply or divide two values that both carry units. calc(2rem * 2rem) is invalid (rem squared is not a length). Multiplication needs at least one side to be a unitless number, and division needs the right-hand side to be unitless.

calc() with CSS custom properties

This is where calc() earns its keep in a real design system. Define a spacing token once as a custom property, then derive everything from it:

css
:root {
  --gap: 1rem;
}

.card {
  padding: var(--gap);
  margin-bottom: calc(var(--gap) * 2);   /* 2rem */
}

.tight {
  gap: calc(var(--gap) / 2);              /* 0.5rem */
}

Change --gap in one place and the whole scale moves. Two things to keep straight. First, a custom property holds an unresolved token, so the multiplication happens at calc() time, not when the variable is declared. Second, if a custom property is empty or invalid, the calc() it feeds becomes invalid too, so give tokens sane defaults. If you read or write these variables from JavaScript, see getting and setting CSS custom properties with JavaScript for the getComputedStyle and setProperty round-trip and the gotchas around it.

min() and max(): caps and floors

min() and max() take a comma-separated list of values and resolve to the smallest or largest at layout time. The names are counterintuitive until you say them out loud: max() sets a floor (never smaller than this), min() sets a cap (never larger than this).

css
/* never wider than 60rem, but shrink below that on small screens */
.container {
  width: min(100%, 60rem);
}

/* a touch target that is at least 44px no matter what the % works out to */
.btn {
  width: max(44px, 10%);
}

min(100%, 60rem) is the modern, media-query-free replacement for the old width: 60rem; max-width: 100%; pair: it reads as "whichever is smaller, the full available width or 60rem." You can mix units freely inside them, and you can nest calc() inside, which is where they get powerful:

css
/* fill the viewport minus a sidebar, but cap at a comfortable reading width */
.reading {
  width: min(calc(100vw - 16rem), 70ch);
}

clamp(): fluid type and spacing without media queries

clamp() is the one most people search for, because it solves fluid typography in a single line. The signature is clamp(MIN, PREFERRED, MAX): the browser uses the preferred value but never lets it drop below MIN or rise above MAX.

css
h1 {
  /* never smaller than 1.5rem, never larger than 3rem,
     scales with the viewport in between */
  font-size: clamp(1.5rem, 1rem + 3vw, 3rem);
}

The middle term is what makes it fluid. A bare 5vw would keep growing on a 4K monitor and shrink to nothing on a phone, which is why the MIN and MAX bounds exist. The 1rem + 3vw form (a fixed base plus a viewport-relative slope) scales more gently than raw vw alone and is the pattern I reach for, because the rem part keeps the text legible when the viewport is small.

Internally clamp(MIN, VAL, MAX) is exactly max(MIN, min(VAL, MAX)), so if you ever forget the argument order, that equivalence tells you which bound is which. It works just as well for spacing as for type:

css
/* section padding that breathes on desktop and tightens on mobile,
   no @media block needed */
section {
  padding-block: clamp(2rem, 5vw, 6rem);
}

One accessibility note: avoid a clamp() whose only fluid term is a vw unit on font-size, because a viewport-only value can defeat browser zoom for low-vision users. Keeping a rem in the middle term (as above) preserves zoom, which is the other reason 1rem + 3vw beats a bare 4vw.

These functions compose with everything else you already use. They are common in grid-template-columns (minmax() is its own grid-specific cousin), in gap, and in layout offsets, so they pair naturally with the difference between position: fixed and position: sticky when you are sizing a pinned sidebar, and with how to center anything in CSS when a clamped width needs to sit dead center in its container.

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsCSScalcclampminmaxfluid typographyresponsive designcustom properties

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts