WasiLearn Code
CSS · 2. Colour, text and units

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 * 16px is fine. 2px * 16px is not — square pixels are not a thing CSS can lay out.
  • Division needs a plain number on the right. 100% / 3 is fine. 100% / 3px is 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:

  1. Look for the strike-through in devtools. If it is there, the value never parsed and it is almost certainly the spacing around + or -.
  2. Check both sides of every + and - for a real space.
  3. Check that * has a plain number on one side and / has one on the right.
  4. 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 a calc() 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