CSS Functions
🔢 Chapter 8 — CSS Functions: calc(), clamp(), min(), max()
CSS was originally a list of property-value pairs with no arithmetic — if you needed a width that was "the full container minus a 280px sidebar", you had to calculate it yourself and hardcode the result. CSS functions changed that. calc() lets CSS do the math at render time. min(), max(), and clamp() add comparisons. Together they unlock truly adaptive values that respond to the viewport, to their container, and to other CSS values — without a single media query needed.
1 — calc()
calc() evaluates a mathematical expression and produces a CSS value. Its superpower is mixing units that CSS cannot otherwise combine — like percentages and pixels.
/* Basic arithmetic — + - * / */
.box {
width: calc(100% - 2rem); /* subtract fixed gutter from fluid width */
height: calc(100vh - 64px); /* full viewport minus a fixed nav height */
padding: calc(1rem + 4px); /* rarely useful, but valid */
font-size: calc(1rem * 1.25); /* multiply by unitless number */
width: calc(100% / 3); /* divide by unitless number */
}
/* Custom properties inside calc() */
:root { --sidebar-width: 260px; }
.main { width: calc(100% - var(--sidebar-width)); }
/* Change --sidebar-width and .main updates automatically */
+ and - operators MUST be surrounded by spaces: calc(100% - 2rem) ✓ — calc(100%-2rem) ✗ (parsed as "100%" minus "2rem" fails, because the parser sees it as a single token). Multiplication and division do not require spaces but spaces are still good practice.The four operators and their rules
calc() result
box-sizing: border-box (which handles padding only), or JavaScript, or approximate values. calc() lets you freely combine percentages, px, rem, em, vw, vh, and any other unit in a single expression — calculated fresh every time the layout changes.
Common calc() patterns
/* Full-bleed content area with capped max-width */
.content {
width: calc(min(100%, 1200px) - 2 * 1.5rem);
margin: 0 auto;
}
/* Offset a sticky element below a fixed nav */
.sticky-sidebar {
position: sticky;
top: calc(var(--nav-height) + 1rem);
max-height: calc(100vh - var(--nav-height) - 2rem);
}
/* Grid column that leaves room for gap */
.grid-item {
width: calc(33.333% - (2 * 16px / 3));
/* 3 items per row, 16px gap between them */
}
/* Negative calc() for overlapping pulls */
.pull-left {
margin-left: calc(-1 * var(--container-padding));
width: calc(100% + var(--container-padding));
}
2 — min() and max()
min() and max() take two or more comma-separated values and return the smallest or largest respectively. This gives you responsive constraints without media queries.
/* min() — "use the smaller of these values" */
.container {
width: min(500px, 100%);
/* On wide screens: 500px (100% would be bigger, so min picks 500px) */
/* On narrow screens: 100% (100% is smaller than 500px, min picks it) */
/* Equivalent to: width: 100%; max-width: 500px; */
}
/* max() — "use the larger of these values" */
.sidebar {
width: max(200px, 25%);
/* On wide screens: 25% (25% is bigger than 200px) */
/* On narrow screens: 200px floor (25% shrinks below 200px) */
/* Equivalent to: width: 25%; min-width: 200px; */
}
/* Font size that never drops below 16px */
p { font-size: max(16px, 2vw); }
/* At 600px viewport: 2vw = 12px → max picks 16px (floor) */
/* At 1000px viewport: 2vw = 20px → max picks 20px (grows) */
/* More than two arguments */
.box { width: min(300px, 50%, 80vw); } /* smallest of three */
Equivalences — min/max as shorthand for min/max-width
.box {
width: 100%;
max-width: 800px;
}
.box {
width: min(800px, 100%);
}
3 — clamp()
clamp(minimum, preferred, maximum) is the most powerful of the three. It locks a value between a floor and a ceiling, with a preferred middle value that can be fluid. Think of it as "min and max combined into one expression."
/* clamp( MINIMUM, PREFERRED, MAXIMUM ) */
/* ↑ floor ↑ ideal ↑ ceiling */
/* */
/* The browser evaluates preferred. */
/* If preferred < minimum → uses minimum. */
/* If preferred > maximum → uses maximum. */
/* Otherwise → uses preferred. */
h1 { font-size: clamp(1.5rem, 4vw, 3rem); }
/* At 320px viewport: 4vw = 12.8px → 1.5rem (24px) floor applies */
/* At 768px viewport: 4vw = 30.7px → fluid (30.7px) in between */
/* At 1200px viewport: 4vw = 48px → 3rem (48px) ceiling applies */
/* Mathematical identity — these are equivalent: */
clamp(a, b, c) /* same as → */ max(a, min(b, c))
rem for the minimum and maximum values (not px). This respects the user's browser font-size preference — if they've set a larger default, your minimum and maximum scale accordingly. A user who needs larger text for accessibility will get it.4 — Fluid Typography with clamp()
The most celebrated use of clamp() is fluid typography: font sizes that smoothly scale with the viewport instead of jumping at breakpoints. A single declaration replaces a font-size rule plus multiple @media overrides.
/* Fluid type scale using clamp()
Formula: clamp(min, preferred-vw, max)
Preferred ≈ the vw fraction where the size feels right at "normal" viewports */
:root {
/* base text — 16px → 20px */
--text-base: clamp(1rem, 1.5vw + 0.5rem, 1.25rem);
/* small label — 12px → 14px */
--text-sm: clamp(0.75rem, 1vw + 0.25rem, 0.875rem);
/* h4 — 18px → 22px */
--text-h4: clamp(1.125rem, 2vw + 0.25rem, 1.375rem);
/* h3 — 22px → 28px */
--text-h3: clamp(1.375rem, 2.5vw + 0.5rem, 1.75rem);
/* h2 — 28px → 40px */
--text-h2: clamp(1.75rem, 3.5vw + 0.5rem, 2.5rem);
/* h1 — 36px → 60px */
--text-h1: clamp(2.25rem, 5vw + 0.5rem, 3.75rem);
/* Hero title — 48px → 96px */
--text-hero: clamp(3rem, 7vw + 0.5rem, 6rem);
}
body { font-size: var(--text-base); }
h1 { font-size: var(--text-h1); }
/* etc. — no media queries needed for typography */
Xvw + Yrem is a linear interpolation: the vw part provides the slope (grows with viewport), the rem part shifts the baseline up or down. Adjust the vw coefficient for steeper or gentler growth.font-size: 4vw (no clamp) means on a 300px screen the text is 12px (illegible), and on a 2400px screen it's 96px (absurd). clamp() is not optional here — the min floor guarantees readability, the max ceiling prevents absurdly large text.
5 — Fluid Spacing
The same clamp technique applies to padding, margin, and gap. Spacing tokens that grow with the viewport create layouts that feel proportional at every size — more breathable on desktop, tighter and more efficient on mobile.
:root {
/* Space scale — each step roughly doubles */
--space-xs: clamp(0.25rem, 0.5vw, 0.5rem);
--space-sm: clamp(0.5rem, 1vw, 0.875rem);
--space-md: clamp(1rem, 2vw, 1.5rem);
--space-lg: clamp(1.5rem, 3.5vw, 3rem);
--space-xl: clamp(2.5rem, 6vw, 5rem);
--space-2xl: clamp(4rem, 10vw, 8rem);
}
/* Use tokens throughout — spacing adapts automatically */
.section { padding: var(--space-xl) var(--space-lg); }
.card { padding: var(--space-md); }
.card-grid { gap: var(--space-md); }
h2 { margin-bottom: var(--space-sm); }
6 — Nesting Functions
calc(), min(), max(), and clamp() can all be nested inside each other. This enables compound expressions that would otherwise require JavaScript or multiple CSS rules.
/* Container with fluid padding AND a maximum width */
.container {
width: min(calc(100% - 2 * var(--space-lg)), 1200px);
margin: 0 auto;
/* "width minus gutters, but never wider than 1200px" */
}
/* Sidebar with a fluid preferred width, floored and ceilinged */
.sidebar {
width: clamp(180px, calc(25% + 2rem), 320px);
/* preferred: 25% of parent + 2rem, clamped 180px→320px */
}
/* Safe area aware padding (notch/rounded-corner phones) */
.page {
padding-bottom: max(env(safe-area-inset-bottom), 1rem);
/* at least 1rem, but respect the phone's safe area */
}
/* Responsive border-radius that scales but stays reasonable */
.card {
border-radius: clamp(8px, calc(0.5vw + 4px), 20px);
}
/* Line-height that scales with font-size (relative calc) */
p {
font-size: clamp(1rem, 1.5vw, 1.25rem);
line-height: calc(1.4 + 0.2 * (1.25rem - 1rem) / 0.25rem);
/* line-height tightens slightly as font size grows */
}
clamp(min, Xvw + Yrem, max) is itself a nested expression: Xvw + Yrem is a calc()-style expression written inline (CSS allows the arithmetic inside min/max/clamp without an explicit calc() wrapper). This is often written without calc(): clamp(1rem, 4vw + 0.5rem, 3rem) — the browser recognises the expression automatically.
7 — Level 4 Math Functions CSS Level 4
CSS Values and Units Level 4 defines additional math functions. Browser support is growing (most landed in 2023–2024). You may encounter these in modern codebases.
| Function | Returns | Example |
|---|---|---|
round(strategy, value, step) | value rounded to nearest step | round(nearest, 17px, 4px) → 16px |
mod(a, b) | Remainder (same sign as divisor) | mod(10px, 3px) → 1px |
rem(a, b) | Remainder (same sign as dividend) | rem(-10px, 3px) → -1px |
abs(a) | Absolute value | abs(-2rem) → 2rem |
sign(a) | -1, 0, or 1 | sign(-5px) → -1 |
sin(a) / cos(a) | Trigonometric (unitless 0–1) | Used in animations and transforms |
pow(a, b) | a raised to power b | pow(2, 3) → 8 |
sqrt(a) | Square root | sqrt(9) → 3 |
/* round() — snap values to a grid (e.g. 4px design grid) */
.box {
width: round(nearest, 33.333%, 4px);
/* Snaps the 1/3 width to the nearest 4px increment */
}
/* sin/cos — position elements on a circle (no JS needed) */
.clock-hand {
/* Rotate items around a centre using trigonometric placement */
transform: translate(
calc(var(--radius) * cos(var(--angle))),
calc(var(--radius) * sin(var(--angle)))
);
}
/* abs() — ensure a positive distance regardless of direction */
.tooltip {
margin-top: abs(var(--offset));
}
round(), mod(), abs(), and trig functions are supported in Chrome 111+, Firefox 118+, Safari 15.4+ — but verify for your specific target browsers.8 — Quick Reference
| Function | Takes | Returns | Primary use |
|---|---|---|---|
calc(expr) |
Math expression with units | Computed value | Mix units, subtract fixed amounts from fluid widths |
min(a, b, …) |
2+ comma-separated values | Smallest value | Maximum constraint — "no wider than X" |
max(a, b, …) |
2+ comma-separated values | Largest value | Minimum constraint — "never smaller than X" |
clamp(min, pref, max) |
3 values: floor, ideal, ceiling | Clamped preferred | Fluid typography, fluid spacing, responsive sizing |
Whitespace rules in calc()
| Operator | Spaces required? | Example |
|---|---|---|
+ and - | Yes — mandatory | calc(100% - 2rem) ✓ calc(100%-2rem) ✗ |
* and / | No — optional | calc(2 * 1rem) = calc(2*1rem) |
clamp() — fluid typography quick formula
/* For font sizes: min in rem, vw for preferred, max in rem */
/* Preferred rule of thumb: choose vw so the preferred falls */
/* nicely between min and max at a typical 768px–1024px screen. */
/* */
/* At 800px: 4vw = 32px → if target heading is 30-34px, good. */
/* Verify your values: */
/* At min: vw_at_min_viewport ≤ minimum */
/* At max: vw_at_max_viewport ≥ maximum */
clamp( 1.5rem, 4vw, 3rem ) /* h2: 24px → 48px */
clamp( 1rem, 2vw, 1.25rem) /* body: 16px → 20px */
clamp( 1rem, 2vw + 0.5rem, 1.5rem) /* compound preferred */
✏️ Exercises
Write the CSS from scratch before checking the solution. Focus on picking the right function for each constraint — not all problems need clamp().
calc() with a --header-height custom property set to 64px.clamp() handles both the floor and ceiling. For the content padding, calc(var(--header-height) + 1rem) adds comfort space below the header.:root { --header-height: 64px; }
/* (a/b/c): 90% width, clamped between 280px and 1100px */
.container {
width: clamp(280px, 90%, 1100px);
margin: 0 auto;
}
.header {
position: sticky;
top: 0;
height: var(--header-height);
z-index: 100;
}
.page-content {
padding-top: calc(var(--header-height) + 1rem);
}
--text-sm (12px → 14px), --text-base (16px → 20px), --text-lg (20px → 28px), --text-xl (28px → 40px), --text-2xl (40px → 64px). Use rem units for min and max. Apply each to the corresponding element type (small, body, h3, h2, h1).rem offset to the preferred to shift the curve.:root {
--text-sm: clamp(0.75rem, 0.8vw + 0.2rem, 0.875rem);
--text-base: clamp(1rem, 1.5vw + 0.25rem, 1.25rem);
--text-lg: clamp(1.25rem, 2.5vw + 0.25rem, 1.75rem);
--text-xl: clamp(1.75rem, 4vw + 0.25rem, 2.5rem);
--text-2xl: clamp(2.5rem, 6.5vw + 0.5rem, 4rem);
}
small { font-size: var(--text-sm); }
body { font-size: var(--text-base); }
h3 { font-size: var(--text-lg); }
h2 { font-size: var(--text-xl); }
h1 { font-size: var(--text-2xl); }
width: 100%; max-width: 720px with min() — (b) replace font-size: 2vw; min-width: 18px (font-size) effectively → use max() so the font never goes below 18px — (c) replace a three-property responsive sidebar (width: 25%; min-width: 180px; max-width: 300px) with a single clamp()./* (a) width: 100%; max-width: 720px → min() */
.container {
width: min(720px, 100%);
/* fluid at narrow viewports, capped at 720px when wide */
}
/* (b) font-size: 2vw but never below 18px → max() */
p {
font-size: max(18px, 2vw);
/* 2vw is used when larger, 18px is the floor */
}
/* (c) sidebar: width:25%, min 180px, max 300px → clamp() */
.sidebar {
width: clamp(180px, 25%, 300px);
/* 25% preferred, floored at 180px, capped at 300px */
}
calc()), have fluid padding top/bottom that scales from 2rem to 5rem (clamp()), contain a heading with fluid type from 2rem to 5rem (clamp()), and a subtitle from 1rem to 1.5rem (clamp()). Define --nav-height: 70px as a custom property and use it in the height calc.height: calc(100vh - var(--nav-height)) for the hero height. Choose vw values for the clamp preferred that feel good at a typical 900px viewport.:root { --nav-height: 70px; }
.hero {
height: calc(100vh - var(--nav-height));
padding-block: clamp(2rem, 6vw, 5rem);
display: flex;
flex-direction: column;
justify-content:center;
align-items: center;
text-align: center;
}
.hero-heading {
font-size: clamp(2rem, 5vw + 0.5rem, 5rem);
margin: 0 0 clamp(0.75rem, 1.5vw, 1.5rem);
}
.hero-subtitle {
font-size: clamp(1rem, 1.8vw + 0.25rem, 1.5rem);
}