
CSS calc(): Doing Math in Your Stylesheets
For years, one of the most common frustrations in CSS was simple arithmetic. You wanted a sidebar that was "the full width minus 300 pixels," or a content column that was "half the screen minus the gutter," and CSS had no way to say it. You either reached for JavaScript, nested an extra wrapper element with padding, or picked a number that looked right at one screen size and broke at every other.
calc() fixed that. It lets you write a mathematical expression anywhere CSS expects a number, length, percentage, angle, or time, and the browser works out the result at layout time using the real, current values. This guide covers how calc() works, the syntax rules that trip people up, the patterns where it earns its place, and how it fits alongside newer math functions like clamp() and the trigonometric functions.
What calc() Actually Does
calc() takes a single expression and returns a computed value. The key feature is that the expression can mix units. The browser can't add 50% and 20px while parsing your stylesheet, because it doesn't yet know what 50% refers to. So it keeps the expression around and resolves it once layout knows the size of the containing block.
.sidebar {
width: calc(100% - 300px);
}
On a 1200px-wide container, this resolves to 900px. On a 700px container, it resolves to 400px. You wrote one rule, and it stays correct at every size.
That deferred resolution is what makes calc() different from a preprocessor like Sass. Sass math runs once, at build time, and can only combine values it already knows. calc() runs in the browser, with live values, including percentages, viewport units, font-relative units like em and rem, and custom properties.
The Syntax Rules
calc() supports four operators: +, -, *, and /. There are a few rules around them that matter.
Whitespace Around + and - Is Required
This is the rule that bites everyone at least once:
/* Invalid: the whole declaration is dropped */
.box {
width: calc(100%-20px);
}
/* Valid */
.box {
width: calc(100% - 20px);
}
Without spaces, the parser reads -20px as a negative number sitting next to 100%, with no operator between them. The declaration is invalid and silently ignored. Multiplication and division don't require whitespace, but adding it anyway keeps things readable and consistent.
Multiplication and Division Need a Plain Number on One Side
You can multiply a length by a number, and divide a length by a number:
.card {
padding: calc(1rem * 1.5); /* valid: 1.5rem */
width: calc(100% / 3); /* valid: one third */
}
What you traditionally couldn't do is multiply two lengths together (calc(10px * 10px) has no meaning as a length) or divide by a length (calc(100px / 2px)). The CSS Values Level 4 spec does allow unit division that produces a plain number, and support for typed arithmetic has started landing in Chromium-based browsers, but it isn't something to rely on across all browsers yet. For portable code, keep one side of every * and the right side of every / unitless.
Division by zero is also worth knowing about. In current browsers, calc(10px / 0) produces an infinite value that gets clamped to the largest length the browser supports rather than failing, which can produce surprisingly huge elements. It rarely happens by accident unless a custom property resolves to 0.
Operator Precedence and Nesting
The usual math precedence applies: * and / before + and -. Use parentheses to group, and you can nest calc() inside calc(), although plain parentheses do the same job:
.column {
/* These two are equivalent */
width: calc((100% - 2 * 1.5rem) / 3);
width: calc(calc(100% - calc(2 * 1.5rem)) / 3);
}
Nested calc() is mostly useful when the inner part comes from a custom property that itself contains a calc() expression. The browser flattens it all into one calculation.
Practical Patterns
Here's where calc() actually earns its keep in day-to-day work.
Fixed Sidebar, Fluid Content
A classic two-column layout where one column has a fixed width and the other takes the rest:
<div class="layout">
<aside class="layout__sidebar">Filters</aside>
<main class="layout__main">Results</main>
</div>
.layout {
display: flex;
}
.layout__sidebar {
width: 280px;
flex-shrink: 0;
}
.layout__main {
width: calc(100% - 280px);
}
In a flex or grid layout you'd more often let the layout engine handle this (flex: 1 or grid-template-columns: 280px 1fr), and that's usually the better choice. But calc() remains the tool when the element isn't a flex or grid item, or when you need the value for something other than width, such as positioning an absolutely placed element relative to that sidebar.
Full-Bleed Sections Inside a Centered Container
A common editorial pattern: text sits in a narrow centered column, but certain images or banners should break out to the full viewport width.
<article class="prose">
<p>Intro text in a readable column.</p>
<figure class="full-bleed">
<img src="/images/harbor.jpg" alt="Boats in a harbor at dusk" />
</figure>
<p>More text.</p>
</article>
.prose {
max-width: 42rem;
margin-inline: auto;
}
.full-bleed {
width: 100vw;
margin-inline-start: calc(50% - 50vw);
}
50% is half the width of .prose, and 50vw is half the viewport. The difference is exactly how far the figure must shift left to line up with the viewport edge. One caveat: 100vw includes the vertical scrollbar on platforms that show one, which can cause a small horizontal overflow. Adding overflow-x: clip on a wrapper, or using a grid-based full-bleed layout, avoids that.
Spacing Based on a Custom Property
calc() and custom properties work together beautifully. Define one base unit and derive everything from it:
:root {
--space: 0.5rem;
}
.stack > * + * {
margin-block-start: calc(var(--space) * 3);
}
.card {
padding: calc(var(--space) * 4) calc(var(--space) * 5);
border-radius: calc(var(--space) * 1.5);
}
Change --space once, and the whole spacing scale adjusts. This is also how you convert a unitless custom property into a length:
.progress {
--percent: 64; /* set from HTML or JS */
width: calc(var(--percent) * 1%);
}
You can't write var(--percent)% and have it work, because CSS doesn't concatenate tokens that way. Multiplying by 1% (or 1px, 1deg, 1s) is the standard way to attach a unit.
Nested Border Radius
When one rounded element sits inside another with padding, the inner radius should be smaller for the corners to look concentric:
.card {
--radius: 20px;
--pad: 8px;
border-radius: var(--radius);
padding: var(--pad);
}
.card__media {
border-radius: calc(var(--radius) - var(--pad));
}
If --pad is ever larger than --radius, the result goes negative. A negative border-radius is invalid on its own, but inside calc() the browser clamps out-of-range results to the allowed range, so it resolves to 0 instead of breaking. That clamping is a useful safety net, but you can make intent explicit with max(0px, calc(var(--radius) - var(--pad))).
Sticky Offsets Under a Fixed Header
If a header has a known height and you want sticky elements to sit just beneath it:
:root {
--header-height: 64px;
}
.toc {
position: sticky;
top: calc(var(--header-height) + 1rem);
}
html {
scroll-padding-top: calc(var(--header-height) + 1rem);
}
The scroll-padding-top line means anchor links scroll their target to just below the header instead of hiding it underneath.
Line Height That Tightens as Text Grows
Large headings look better with tighter line spacing. A small calc() expression can derive it from the font size:
h1,
h2 {
line-height: calc(1em + 0.5rem);
}
At a 16px font size, that's 24px (1.5). At 48px, it's 56px (about 1.17). The added fixed amount matters less as the text grows, so leading tightens automatically.
Using calc() in Grid and Animation
calc() works in grid track definitions, although grid's own fr unit handles most cases better:
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(calc(10rem + 5vw), 1fr));
gap: 1rem;
}
It also works in transforms and timing values, which is handy for staggered animations driven by a custom property set on each item:
<ul class="reveal">
<li style="--i: 0">One</li>
<li style="--i: 1">Two</li>
<li style="--i: 2">Three</li>
</ul>
.reveal li {
opacity: 0;
animation: fade-up 400ms ease-out forwards;
animation-delay: calc(var(--i) * 80ms);
}
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(calc(var(--i) * 4px + 8px));
}
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.reveal li {
animation: none;
opacity: 1;
}
}
Chromium-based browsers have also shipped sibling-index(), which would remove the need for the inline --i values, but it isn't available in every browser yet (check caniuse before relying on it), so the custom property approach is still the portable one.
calc() and Its Siblings
calc() was the first CSS math function, but it now has company. The most useful relatives are min(), max(), and clamp(), and they all accept calc()-style expressions directly, without wrapping:
.container {
width: min(100% - 2rem, 70rem);
}
h1 {
font-size: clamp(2rem, 1.2rem + 3vw, 4rem);
}
Notice that 100% - 2rem inside min() doesn't need its own calc(). Every math function argument is already a calculation context.
Beyond those, all current major browsers support:
- Stepped values:
round(),mod(), andrem()for snapping values to increments. - Trigonometry:
sin(),cos(),tan(),asin(),acos(),atan(), andatan2(), useful for circular layouts. - Exponential functions:
pow(),sqrt(),hypot(),log(), andexp(). - Sign-related functions:
abs()andsign(), which reached full cross-browser support later than the others.
Here's a quick example with trigonometry, placing items around a circle:
.dial__item {
--angle: calc(var(--i) * 45deg);
position: absolute;
top: 50%;
left: 50%;
translate: calc(cos(var(--angle)) * 120px - 50%)
calc(sin(var(--angle)) * 120px - 50%);
}
And rounding, to snap a fluid value to a spacing grid:
.block {
padding: round(nearest, 2.5vw, 4px);
}
These newer functions have solid support in current Chrome, Edge, Firefox, and Safari, but if you support older browsers, provide a fixed fallback declaration first.
Common Pitfalls
Forgetting the spaces. calc(100%-1rem) is invalid and gets thrown away without any error. If a calc() declaration seems to do nothing, check the whitespace first. DevTools will show the declaration with a warning icon or strikethrough.
Percentages that refer to something unexpected. A percentage inside calc() means whatever it means for that property. For width it's the containing block's width. For height, it's the containing block's height, which is often auto, in which case the percentage can't resolve and behaves as auto. For padding and margin, even vertical ones, it's the containing block's width. calc() doesn't change any of that.
Unitless zero. In most places 0 and 0px are interchangeable, but inside calc() a bare 0 is a number, not a length. calc(0 + 10px) is invalid because you can't add a number to a length. Write 0px.
Using calc() where the layout engine does it better. If you find yourself writing calc(33.333% - 1.333rem) to fit three columns with gaps, stop and use grid with gap and 1fr tracks. The browser handles the gap math for you and doesn't leave rounding errors.
Overusing it for static values. calc(16px * 2) is just 32px. It's fine as documentation of intent, but a stylesheet full of constant arithmetic is harder to read. calc() is at its best when at least one value is dynamic.
Browser Support
Basic calc() has been supported in every major browser for over a decade. Nesting and use with custom properties are also universally supported. The main differences today are at the edges: typed arithmetic that divides lengths by lengths, and a few of the newer math functions in older browser versions. For anything in this guide besides those, you can use calc() without a fallback.
If you do need a fallback for one of the newer functions, the cascade gives you one for free:
.block {
padding: 1rem;
padding: round(nearest, 2.5vw, 4px);
}
Browsers that don't understand the second declaration discard it and keep the first.
Conclusion
calc() is one of those features that quietly solves a whole category of problems. It lets you combine units that CSS otherwise keeps apart, derive values from custom properties, and describe layouts in terms of relationships ("the viewport minus the header") rather than magic numbers. Remember the whitespace rule, keep one side of multiplication unitless, and reach for grid or flexbox first when the layout engine can do the math for you.
Once you're comfortable with it, the rest of the CSS math family, min(), max(), clamp(), round(), and the trig functions, follows the same rules. They're all just calculations, resolved live by the browser with the values it has at that moment.


