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.

🎨 CSS — calc() syntax and operators
/* 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 */
Whitespace rule: the + 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

+AdditionNeeds spaces around it
-SubtractionNeeds spaces around it
*MultiplicationOne operand must be unitless
/DivisionRight operand must be unitless
calc() — sidebar + main layout
container — 100% width
sidebar 200px
main content
calc() result
The big deal: mixing units. Pure CSS had no way to express "100% of the container, minus 16px of padding." You either had to use 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

🎨 CSS — calc() in the real world
/* 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.

🎨 CSS — min() and max()
/* 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

Classic (two properties)
.box { width: 100%; max-width: 800px; }
Modern (one expression)
.box { width: min(800px, 100%); }
min() vs max() — resize the "container"
min(500px, 100%)
content
Fluid up to 500px, then stops growing
max(200px, 50%)
content
Fluid 50%, but never narrower than 200px

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."

🎨 CSS — clamp() anatomy
/* 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))
Use 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.
clamp() — font size across viewport widths
320px
480px
640px
960px
1440px
Heading text
💡 Colour coding in the demo above: purple = clamped to minimum (floor active), blue = fluid (preferred value in range), green = clamped to maximum (ceiling active). A good clamp value spends most viewport widths in the fluid blue zone.

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.

🎨 CSS — a complete fluid type scale
/* 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 */
The "preferred" value format 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.
⚠️ Don't use vw alone for font sizes. 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.

🎨 CSS — fluid spacing tokens
: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.

🎨 CSS — nested function expressions
/* 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 */ }
calc() inside clamp preferred values is especially common. The pattern 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.

FunctionReturnsExample
round(strategy, value, step)value rounded to nearest stepround(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 valueabs(-2rem)2rem
sign(a)-1, 0, or 1sign(-5px)-1
sin(a) / cos(a)Trigonometric (unitless 0–1)Used in animations and transforms
pow(a, b)a raised to power bpow(2, 3)8
sqrt(a)Square rootsqrt(9)3
🎨 CSS — Level 4 math in practice
/* 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)); }
Check caniuse.com before using Level 4 math functions in production. 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

FunctionTakesReturnsPrimary 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()

OperatorSpaces required?Example
+ and -Yes — mandatorycalc(100% - 2rem) ✓    calc(100%-2rem)
* and /No — optionalcalc(2 * 1rem) = calc(2*1rem)

clamp() — fluid typography quick formula

🎨 CSS — choosing clamp parameters
/* 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().

Exercise 1
Create a centred content container that: (a) is always 90% of its parent width, (b) is never wider than 1100px, (c) never narrower than 280px, all in a single CSS declaration using the appropriate function. Then write a sticky header that sits 0px from the top of the viewport, and a content area whose top padding ensures it is never hidden behind the header — using calc() with a --header-height custom property set to 64px.
Hint: For the container, clamp() handles both the floor and ceiling. For the content padding, calc(var(--header-height) + 1rem) adds comfort space below the header.
CSS
: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); }
Exercise 2
Write a full set of fluid typography custom properties for a project. Define: --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).
Hint: Convert px to rem by dividing by 16 (default root font size). A suitable preferred vw for each step: sm≈0.8vw, base≈1.5vw, lg≈2.5vw, xl≈4vw, 2xl≈6.5vw. You may add a rem offset to the preferred to shift the curve.
CSS
: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); }
Exercise 3
Write three rules that replace common two-property patterns with a single expression: (a) replace 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().
Hint: min() = "never wider than", max() = "never smaller than", clamp() = "floor, preferred, ceiling" in one expression.
CSS
/* (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 */ }
Exercise 4
Build a hero section using only CSS functions (no fixed pixel values except for the header height). The hero should: fill the viewport height minus a 70px header (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.
Hint: 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.
CSS
: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); }