Errors and fixes

Tailwind and calc(): CSS variables, underscores and the cases that fail

In an arbitrary Tailwind value a space separates one class from the next. That is why `calc()` needs underscores, and why some expressions stay impossible.

Key points

  • Replace the spaces inside an arbitrary value with `_`: `h-[calc(100dvh_-_var(--header-h))]`.
  • Read theme tokens with `theme()` in v3 and with CSS variables in v4: do not mix them in one calculation.
  • When an expression is long or reused, a plain CSS class is more readable and quicker to debug.

Short answer: inside an arbitrary Tailwind value a space separates one class from the next, so calc() needs underscores in place of spaces. h-[calc(100dvh_-_var(--header-h))] is the correct form.

The mechanism: a space closes the token

Tailwind reads the class attribute and splits it on whitespace before it interprets anything. Square brackets do not protect the content: the parser takes everything up to the first space and reads the rest as another class.

For h-[calc(100vh - 4rem)] the candidate becomes h-[calc(100vh, which matches no utility, and - plus 4rem)] get discarded. No error, no warning: the declaration is simply never written.





The underscore is the escape character: it turns back into a space when Tailwind writes the rule. A literal underscore is written with a backslash, \_. The rule holds in v3 and in v4.

Diagnosis: read the generated sheet

The browser is the last link in the chain and does not know what you typed. Build the CSS once and search for the class: a missing rule means a syntax problem, a rule without the property means a variable problem.

npx @tailwindcss/cli -i src/app.css -o out.css --content "./src/**/*.tsx"
grep -o "\.h-\\[calc[^{]*" out.css | head

When the rule is there but the value is not applied, the usual cause is a var() that was never declared: the declaration becomes invalid and the property falls back to its initial value, which for height is auto. The Computed panel of the developer tools shows the declaration struck through, which proves the syntax was right.

With a CSS variable

:root {
  --header-h: 4rem;
}

  ...

100dvh beats 100vh on mobile: the dynamic height follows the address bar and stops cutting content when the browser changes size. The variable must sit on an ancestor, never on a sibling.

Theme tokens: v3 and v4 do not mix

VersionHow to read a tokenExample
Tailwind 3theme()w-[calc(100%-theme(spacing.4))]
Tailwind 4CSS variablew-[calc(100%-var(--spacing-4))]

In Tailwind 4 theme tokens are CSS variables. theme() stays for compatibility, but var() is the native form. Mixing the two inside one calculation produces a value the browser cannot resolve, and the symptom is once again a wrong height with no error.

When a plain CSS class wins

If the calculation appears in three places, or changes with the layout, a semantic class is easier to read and easier to inspect:

.app-shell {
  min-height: calc(100dvh - var(--header-h));
}

The arbitrary class is still fine for a one-off. It is not a replacement for CSS when the expression is part of the layout and other people have to read it.

When the usual fix makes it worse

The first reaction is to add !important or raise specificity with a compound selector. It does nothing: when the candidate was never interpreted there is no rule to make important, and meanwhile the new rule overrides the responsive variants you wanted.

The second reaction is to move the calculation into an inline style attribute. It works, but you lose every variant, md:, dark:, motion-safe:, and the value leaves the theme, so two components with the same calculation drift apart at the first change.

One limit to budget for: unregistered properties

An unregistered custom property is an opaque string: the browser does not know it holds a length, so it cannot interpolate it. An animation on --header-h jumps instead of sliding until you declare @property with syntax: "<length>" and an initial value. The second limit is that calc() does not validate units: adding rem to px is fine, adding a number to a length is not.

Prevention

  • Define the layout measurements, header, sidebar, footer, as variables in one place.
  • Prefer dvh to vh for full-height shells.
  • Leave a comment next to the arbitrary class: the next reader must know why it is there.
  • Keep a single Tailwind version per project: tokens change shape between v3 and v4.

Where to go next

In the catalogue: pricing block, components and icons. The guides: Flexbox and Grid for designers and dark mode without a second design system. The comparison: copy-paste or headless.

Frequently asked questions

Why does h-[calc(100vh - 4rem)] not work?

The space closes the arbitrary value: Tailwind reads calc(100vh and drops the rest. Write h-[calc(100vh_-_4rem)], with one underscore per space.

Do CSS variables work inside calc()?

Yes, and the variable has to be defined on an ancestor of the element. When it is missing the declaration turns invalid and the property falls back to its initial value.

Can I use theme() inside calc()?

In Tailwind 3 yes, for theme tokens. In Tailwind 4 the tokens are CSS variables, so use var(--spacing-4) instead of theme(spacing.4) and never mix the two forms.

Is a dedicated CSS class better?

If the calculation shows up in three places, or changes with the layout, yes. A class such as .app-shell reads and inspects better than the arbitrary string.

Does the same rule apply to min(), max() and clamp()?

Yes, underscores work there too. text-[clamp(1rem,2vw,1.5rem)] holds no spaces, so it needs no change.