Tailwind CSS Handbook v4.3

Arbitrary Values

Square-bracket values, arbitrary properties and variants, the CSS-variable shorthand, underscores and type hints — and when to reach for @theme instead.

Every utility in Tailwind has a square-bracket form that accepts any value you like. It is the release valve that stops a design system from becoming a straitjacket — and the feature most likely to erode that system if you lean on it.

Overview#

HTML
<div class="top-[117px] bg-[#bada55] text-[22px]">…</div>
Generated
.top-\[117px\] { top: 117px; }
.bg-\[\#bada55\] { background-color: #bada55; }
.text-\[22px\] { font-size: 22px; }

Arbitrary values compose with variants exactly like ordinary utilities:

HTML
<div class="top-[117px] lg:top-[344px] hover:bg-[#bada55]">…</div>

Arbitrary values#

Any utility that takes a value takes an arbitrary one. The bracket contains raw CSS for that property.

Loading preview…

Anything the browser accepts for that property is legal — calc(), clamp(), color-mix(), min(), gradients, env():

HTML
<div class="w-[clamp(20rem,50vw,40rem)] pt-[env(safe-area-inset-top)]">…</div>

Arbitrary properties#

When there is no utility at all, put the whole declaration in brackets:

HTML
<div class="[mask-type:luminance]">…</div>
<div class="[mask-type:luminance] hover:[mask-type:alpha]">…</div>

The same syntax sets a CSS custom property, which is how you feed a value into hand-written CSS or a child component from markup:

HTML
<div class="[--scroll-offset:56px] lg:[--scroll-offset:44px]">…</div>

This is more useful than it first looks: the variable can be set responsively, per state, per breakpoint — things an inline style attribute cannot do.

HTML
<div class="[--card-gap:0.5rem] md:[--card-gap:1.5rem] gap-(--card-gap) flex">…</div>

Arbitrary variants#

Square brackets in front of the colon write a selector instead of a value.

HTML
<li class="[&:nth-child(-n+3)]:font-semibold">…</li>
<li class="[&.is-dragging]:cursor-grabbing">…</li>
<div class="[&_p]:mt-4">…</div>

At-rules work too, and there & is unnecessary:

HTML
<div class="flex [@supports(display:grid)]:grid">…</div>

Arbitrary variants are covered in depth in Custom Variants, including when to promote one to @custom-variant.

The CSS variable shorthand#

v4 Referencing a variable is common enough to get its own syntax — parentheses instead of brackets:

HTML
<div class="bg-(--my-color)">…</div>

That is shorthand for exactly this:

HTML
<div class="bg-[var(--my-color)]">…</div>

The two are identical in output. The parenthesised form exists because the bracketed one is noisy and easy to mistype, and because var( and ) in a class name make search-and-replace unpleasant.

Whitespace and underscores#

Class names cannot contain spaces, so Tailwind reads _ as a space at build time:

HTML
<div class="grid grid-cols-[1fr_500px_2fr]">…</div>
<div class="shadow-[0_35px_60px_-15px_rgba(0,0,0,0.3)]">…</div>
Loading preview…

When underscores are preserved#

Where a space would be invalid CSS but underscores are common, Tailwind leaves the underscore alone. URLs are the main case:

HTML
<div class="bg-[url('/what_a_rush.png')]">…</div>

Escaping an underscore#

If you genuinely need a literal underscore where a space would be valid, escape it with a backslash:

HTML
<div class="before:content-['hello\_world']">…</div>

The JSX caveat#

JSX strips the backslash before Tailwind ever sees the class, so the escape silently stops working. Use String.raw to preserve it:

JSX
<div className={String.raw`before:content-['hello\_world']`}>…</div>

Type hints#

Some prefixes serve more than one CSS property. text- can mean font size or colour; bg- can mean colour, image or size. With a literal value Tailwind infers correctly, but with a CSS variable it cannot see the value, so you tell it:

HTML
<!-- Font size -->
<div class="text-(length:--my-var)">…</div>

<!-- Colour -->
<div class="text-(color:--my-var)">…</div>

The bracket form takes the same hint:

HTML
<div class="text-[color:var(--my-var)]">…</div>
<div class="bg-[length:200px_100px]">…</div>
<div class="bg-[image:var(--my-gradient)]">…</div>
HintForces the utility to mean
color:A colour
length:A length — font size, background size
image:A background image
url:A URL
angle:An angle
position:A background position
family-name:A font family
number:A unitless number
percentage:A percentage

Theme variables inside arbitrary values#

Arbitrary values are the natural place to do arithmetic on your design tokens, because @theme emits real CSS variables:

HTML
<!-- One pixel inside the parent's radius, so nested corners look right -->
<div class="rounded-[calc(var(--radius-xl)-1px)]">…</div>

<!-- A height that subtracts a fixed header -->
<main class="min-h-[calc(100dvh-var(--header-height))]">…</main>

The --spacing() function works here too:

HTML
<div class="py-[calc(--spacing(4)-1px)]">…</div>

Remember the underscore rule: calc() arguments need spaces around operators, so write calc(100%_-_2rem), not calc(100%-2rem) — the second is not valid CSS once the underscores are gone.

HTML
<div class="w-[calc(100%_-_2rem)]">…</div>

When not to use arbitrary values#

Arbitrary values are for the genuinely one-off. The moment a value appears twice, it is a token pretending not to be one.

HTML
<!-- The smell: the same magic number in three places -->
<header class="h-[68px]">…</header>
<aside class="top-[68px]">…</aside>
<main class="pt-[68px]">…</main>
app.css — the fix
@theme {
  --spacing-header: 68px;
}
HTML
<header class="h-header">…</header>
<aside class="top-header">…</aside>
<main class="pt-header">…</main>

Now there is one place to change it, the name says what it is, and autocomplete offers it.

SituationUse
A value used once, in one componentArbitrary value
A value used two or more timesA @theme token
A value your design system already namesThe existing utility
A property with no utility, used onceArbitrary property
A property with no utility, used repeatedly@utility
A selector you write more than twice@custom-variant

Other reasons to be sparing#

  • They are invisible to your design system. Nobody auditing @theme will find bg-[#bada55].
  • They do not respond to theming. A hard-coded hex does not change in dark mode; bg-surface does.
  • They read poorly. shadow-[0_35px_60px_-15px_rgba(0,0,0,0.3)] in the middle of a class list is a wall of noise compared to shadow-2xl.
  • They defeat class-name search. You cannot grep for "every place we used the brand colour" once it is spelled three different ways.

Common mistakes#

  • Constructing the class name dynamically. ` top-[${offset}px] produces nothing, because Tailwind scans source files as plain text. Set a CSS variable instead: class="top-(--offset)" style="--offset: 117px"`.
  • Spaces instead of underscores. grid-cols-[1fr 500px] is two class names, not one value.
  • Forgetting spaces around calc() operators. w-[calc(100%-2rem)] compiles to invalid CSS. Write w-[calc(100%_-_2rem)].
  • Losing the escape in JSX. Use String.raw for backslash escapes inside template literals.
  • Omitting a type hint with a variable. text-(--my-var) is ambiguous; use text-(color:--my-var) or text-(length:--my-var).
  • Reaching for arbitrary values before checking the scale. p-[16px] is p-4; text-[16px] is text-base.
  • Using them for anything themed. Colours, radii and spacing almost always belong in @theme.