CSS functions: calc(), clamp(), min(), max()
CSS math functions calc(), min(), max(), and clamp() compute values at layout time, letting you mix units like %, px, and vw in a single declaration without JavaScript.
Theory
TL;DR
calc()does arithmetic across units:width: calc(100% - 250px)subtracts a fixed sidebar from full widthmin(a, b)picks the smaller value;max(a, b)picks the larger oneclamp(min, val, max)means "use val, but never below min or above max"- Think of a thermostat:
min/maxenforce one boundary;clampenforces both;calcdoes the math in between - Decision rule: custom math →
calc(), one boundary →min/max, both boundaries →clamp()
Quick example
.card {
/* Old approach: two separate declarations */
width: 100%;
max-width: 600px;
/* Modern: one declaration handles everything */
width: clamp(300px, 50vw, 600px); /* min 300px, ideal 50vw, max 600px */
font-size: min(2rem, 5vw); /* scales with viewport, never too large */
padding: calc(var(--gutter) * 2 + 1rem); /* theme-aware spacing */
}clamp(300px, 50vw, 600px) returns 50vw on most screens, drops to 300px on small ones, and caps at 600px on wide ones. One line replaces two media query breakpoints.
Key difference
calc() evaluates an expression and returns the result. min() and max() pick from a list of resolved values. clamp(min, val, max) is equivalent to max(min, min(val, max)) but reads more clearly. All four are evaluated during layout, not at parse time, so the browser recalculates on every reflow triggered by resize or DOM changes.
When to use
- Fluid typography →
clamp():font-size: clamp(1rem, 2.5vw, 1.5rem)grows smoothly across screen sizes, no breakpoints needed - Container width with a cap →
min(100% - 2rem, 800px): full width on mobile, fixed maximum on desktop - Offset-based sizing →
calc():height: calc(100vh - 80px)accounts for a fixed header - Minimum guarantee →
max():padding: max(1rem, 2vmin)never collapses on small screens - Theme-aware spacing →
calc()with custom properties:calc(var(--gutter) * 2 + 1rem)
Comparison table
| Function | Syntax | Returns | Typical use |
|---|---|---|---|
calc() | calc(expression) | Result of math ops | width: calc(100% - 250px) |
min() | min(val1, val2, ...) | Smallest value | font-size: min(3rem, 8vw) |
max() | max(val1, val2, ...) | Largest value | padding: max(1rem, 2vmin) |
clamp() | clamp(min, val, max) | val clamped to [min, max] | width: clamp(320px, 90vw, 1200px) |
| When to use | Custom math | Pick one bound | Enforce a range |
How the browser handles this
Browsers parse these functions in the CSSOM during style resolution. calc() resolves expressions after converting % and viewport units to logical pixels at layout time. min/max/clamp compare fully resolved lengths at the used-value stage per the CSS Values spec. No JavaScript is involved. The calculation runs again on every reflow, so resize events automatically produce updated values.
Common mistakes
Mistake: no spaces around + and - in calc()
/* Wrong - parser reads 100%-20px as a single token */
width: calc(100%-20px);
/* Correct */
width: calc(100% - 20px);* and / do not require spaces. But + and - without spaces break the rule because the parser cannot tell if the minus is an operator or a sign prefix. This catches most developers at least once.
Mistake: wrong argument order in clamp()
/* Wrong - min is larger than preferred, result is always clamped to 2rem */
font-size: clamp(2rem, 1rem, 4rem);
/* Correct */
font-size: clamp(1rem, 2rem, 4rem);If the minimum exceeds the preferred value, clamp always returns the minimum. The order must satisfy min <= preferred <= max.
Mistake: missing units in min() or max()
/* Wrong - 500 has no unit, the declaration is invalid */
width: min(100%, 500);
/* Correct */
width: min(100%, 500px);Mistake: dividing by a variable that might be zero
/* Wrong - if --cols resolves to 0, result is NaN, falls back to 0, breaks layout */
margin: calc(100% / var(--cols, 0));
/* Correct - guard the variable */
margin: calc(100% / max(1, var(--cols, 1)));Mistake: unnecessary nesting of calc()
/* Works, but redundant */
width: calc(calc(100% - 20px) - 10px);
/* Simpler - nested calc expressions evaluate left-to-right anyway */
width: calc(100% - 30px);Real-world usage
- TailwindCSS v3:
clamp()in arbitrary values, for examplew-[clamp(20rem,50vw,40rem)] - Bootstrap 5.3:
clamp(1rem, 1.5vw, 1.5rem)in fluid font-size utilities - Chakra UI:
clamp(300px, 60vh, 500px)for responsive modal heights - React Native Web:
calc(100vw - ${sidebarWidth}px)via StyleSheet - Replaces
ResizeObserverand JS sizing logic wherever the constraint is purely dimensional
Follow-up questions
Q: Why does calc() require spaces around + and - but not * and /?
A: The CSS parser cannot distinguish - as an operator from - as a sign prefix on a number like -20px. Without spaces, 100%-20px looks like a single token. Multiplication and division have no such ambiguity, so spaces are optional there.
Q: What is the difference between min(90%, 800px) and clamp(0px, 90%, 800px)?
A: In this specific case the result is the same: both return 90% until it hits 800px. The difference is intent. clamp makes an explicit lower bound visible, which matters when that lower bound is meaningful. When you only need a ceiling, min is simpler to read.
Q: width: calc(100% - var(--sidebar)) works on desktop but returns 0px on mobile. Why?
A: The variable --sidebar is probably not defined in the mobile scope or not inherited into that element. When a custom property is unset, calc() resolves to the initial value and collapses the width. Fix: add a fallback calc(100% - var(--sidebar, 0px)) or scope the variable correctly.
Q: Do these functions have a meaningful performance cost?
A: Negligible for static layout. They are evaluated once per layout pass, far faster than equivalent JavaScript. Avoid deep nesting inside CSS animations where recalculation happens every frame, but for sizing and spacing they are fine.
Q (senior): How do container query units work with these functions?
A: calc(100cqw - 40px) sizes relative to the nearest container-type ancestor, not the viewport. cqw resolves at the container's used size during layout. This works in Chrome 105+ and Firefox 110+. clamp(200px, calc(100cqw - 40px), 400px) inside @container is valid and evaluated correctly.
Examples
Responsive container without media queries
.container {
/* Full width on small screens, capped at 800px, side margins included inline */
width: min(100% - 2rem, 800px);
margin-inline: auto;
/* Spacing that scales between 1rem and 3rem based on viewport width */
padding: clamp(1rem, 3vw, 3rem);
}
/* 375px screen: width = 343px (375 - 32). 1200px screen: width = 800px. */min(100% - 2rem, 800px) replaces the classic width: 100%; max-width: 800px; margin: 0 auto pattern. The side margins are handled directly in the expression, so the extra wrapper element is not needed.
Fluid typography system
:root {
--text-sm: clamp(0.875rem, 1vw, 1rem);
--text-base: clamp(1rem, 1.5vw, 1.125rem);
--text-lg: clamp(1.125rem, 2vw, 1.5rem);
--text-xl: clamp(1.5rem, 3vw, 2.5rem);
--text-2xl: clamp(2rem, 4vw, 3.5rem);
}
h1 { font-size: var(--text-2xl); }
p { font-size: var(--text-base); }Every size scales fluidly between its minimum and maximum. No breakpoints. On a 320px screen the base size is 1rem; on a 1440px screen it reaches 1.125rem. The scale is defined once in :root and reused everywhere via custom properties.
Layout with header offset and auto-fit grid
.hero {
/* Viewport height minus a CSS variable for the header height */
min-height: calc(100vh - var(--header-h, 80px));
width: min(100% - 2rem, 960px);
margin-inline: auto;
}
.grid {
display: grid;
/* Columns auto-fit, each at least 250px wide */
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: clamp(1rem, 3vw, 2rem);
}calc(100vh - var(--header-h, 80px)) deducts the header height dynamically. If the variable is not set, the fallback 80px applies. clamp(1rem, 3vw, 2rem) on gap keeps spacing proportional to the viewport without any manual breakpoints.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.