Doing maths in CSS with calc()
The sidebar is 240 pixels. The main panel should take everything else. You cannot write it: 100% is a percentage, 240px is a length, and until the page renders nobody knows what 100% is in pixels — not you, not the stylesheet.
calc() is the one place in CSS where those two can meet.
.main {
width: calc(100% - 240px);
}
The browser resolves 100% at layout time, subtracts 240, and uses the result.
Resize the window and it recalculates. No JavaScript, no media queries, no
guessing.
What you may put inside
calc() accepts +, -, * and /, with brackets for grouping, and it
handles any lengths CSS understands: px, %, rem, em, ch, vw, dvh
and more, mixed freely.
.panel { width: calc(100% - 2rem); }
.column { width: calc(100% / 3); }
.heading { font-size: calc(1rem * 1.5); }
.gutter { padding: calc(1rem + 4px); }
Two restrictions on multiplication and division, and they follow from dimensional sense rather than being arbitrary:
- Multiplication needs a plain number on at least one side.
2 * 16pxis fine.2px * 16pxis not — square pixels are not a thing CSS can lay out. - Division needs a plain number on the right.
100% / 3is fine.100% / 3pxis not.
calc(1rem * 1.5) is legal. Note the 1.5 carries no unit, exactly like the
unitless line-height numbers coming in the next lesson.
The whitespace rule that fails silently
This works:
.panel { width: calc(100% - 32px); }
This does nothing whatsoever:
.panel { width: calc(100%-32px); }
No console error. No warning. No change on screen. The declaration is simply thrown away, and the element falls back to whatever it would have been.
+ and - must have whitespace on both sides. Not a style preference —
a parser requirement, and the reason is genuine.
CSS numbers may carry a sign. -32px is a single valid token meaning negative
thirty-two pixels; that is how margin-left: -32px works. So when the parser
reads 100%-32px it does not see subtraction. It sees 100% followed by
-32px, two values sitting side by side with no operator between them, which
is meaningless inside calc(). The whole expression is invalid, and CSS
discards invalid declarations rather than guessing.
The half-spaced versions fail for exactly the same reason:
width: calc(100% -32px); /* invalid — still reads as two values */
width: calc(100%- 32px); /* invalid */
width: calc(100% - 32px); /* correct */
* and / do not have this problem — calc(100%/3) is valid — because
nothing in CSS begins with * or /. Space them anyway, so your eye reads
all four operators the same way.
The one clue you get is in devtools. Open the Styles panel and a discarded declaration appears struck through, usually with a small warning marker. That strike-through is your entire error message, and once you know to look for it, this bug takes ten seconds instead of an afternoon.
Worth knowing: it is not just calc(). A misspelt property or an impossible
value anywhere in CSS is dropped and struck through in the same way. Getting
into the habit of checking for it will pay back on far more than this.
Thirds, and the rounding you avoid
Three equal columns. The temptation is 33.33%:
.col { width: 33.33%; } /* three of these never quite fill the row */
.col { width: calc(100% / 3); } /* the browser keeps full precision */
A third is 33.333…% and no decimal you type is exact. The shortfall is small but real, and it grows with the container:
| Container | 3 × 33.33% | Left over |
|---|---|---|
| 900px | 899.91px | 0.09px |
| 1200px | 1199.88px | 0.12px |
| 1600px | 1599.84px | 0.16px |
A fraction of a pixel is invisible on its own. It becomes visible as a hairline of background showing through a row of tiles, or as the third column dropping onto a new line when it is one hundredth of a pixel too wide for what is left.
calc(100% / 3) keeps the browser's own precision and the problem does not
arise. The bigger win comes when gaps enter the picture — three columns with a
16px gutter between them, taken out before dividing:
.col {
width: calc((100% - 2 * 16px) / 3);
}
Two gutters, three columns, arithmetic the browser does at layout time. Try expressing that as a single percentage and you cannot, because it depends on a container width you do not know.
Filling the space under a fixed header
The other everyday job. A 64px header at the top; the content area should take the rest of the screen exactly — no page scrollbar, no gap at the bottom:
.site-header {
height: 64px;
}
.main {
height: calc(100dvh - 64px);
overflow-y: auto;
}
100dvh is the visible viewport height right now, so this survives the mobile
address bar sliding in and out. Subtract the header and the content area fits
precisely. overflow-y: auto puts the scrollbar inside .main rather than on
the page, which is usually what a fixed header is for.
Nesting is allowed where it makes the intent clearer:
.main {
height: calc(100dvh - calc(64px + 1rem));
}
Plain brackets would do the same job in fewer characters, and are more common. Both are valid.
min(), max() and clamp() do the arithmetic for you
These three functions accept the same expressions as calc(), so you never
need to write calc inside them:
.card { width: min(100%, 480px); }
.gutter { padding-inline: max(1rem, 3vw); }
.title { font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem); }
min()takes the smaller value, which caps a size — 480px normally, but never wider than the container.max()takes the larger, which enforces a floor.clamp(minimum, preferred, maximum)gives you a value that scales in the middle and stops at both ends. That title grows with the viewport and can never fall below 24px or exceed 48px.
Notice 4vw + 0.5rem inside clamp() with no calc wrapper. The same
whitespace rule still applies: drop the spaces around that + and the entire
declaration is discarded.
min() reads backwards at first — you use it to set a maximum — so say it
out loud: "the smaller of one hundred per cent and four eighty".
When a calc goes quiet
A checklist, in the order that finds it fastest:
- Look for the strike-through in devtools. If it is there, the value never
parsed and it is almost certainly the spacing around
+or-. - Check both sides of every
+and-for a real space. - Check that
*has a plain number on one side and/has one on the right. - If it parsed but the number is wrong, ask what
100%is measuring against — it is the containing block, not the window, and that is a box model question rather than acalc()one.
Next: text styling, where clamp() earns its keep on
headings. If step 4 above is where you keep landing, the units
lesson sets out what each unit actually measures from, and
the combinators lesson covers the case where the rule
was never matching in the first place.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes