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#
<div class="top-[117px] bg-[#bada55] text-[22px]">…</div>.top-\[117px\] { top: 117px; }
.bg-\[\#bada55\] { background-color: #bada55; }
.text-\[22px\] { font-size: 22px; }Arbitrary values compose with variants exactly like ordinary utilities:
<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.
<div class="flex flex-wrap items-end gap-4 p-4">
<div class="grid h-[72px] w-[72px] place-items-center rounded-[14px] bg-[#bada55] text-[13px] font-semibold text-gray-900">72px</div>
<div class="grid h-[104px] w-[104px] place-items-center rounded-[14px] bg-[oklch(0.62_0.19_264)] text-[13px] font-semibold text-white">104px</div>
<div class="grid h-[72px] w-[136px] place-items-center rounded-[14px] bg-[linear-gradient(135deg,#0ea5e9,#6366f1)] text-[13px] font-semibold text-white">gradient</div>
</div>Anything the browser accepts for that property is legal — calc(), clamp(), color-mix(), min(), gradients, env():
<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:
<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:
<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.
<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.
<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:
<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:
<div class="bg-(--my-color)">…</div>That is shorthand for exactly this:
<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:
<div class="grid grid-cols-[1fr_500px_2fr]">…</div>
<div class="shadow-[0_35px_60px_-15px_rgba(0,0,0,0.3)]">…</div><div class="p-4">
<div class="mx-auto grid max-w-lg grid-cols-[1fr_120px_2fr] gap-3 text-center text-sm font-medium">
<div class="rounded-lg bg-indigo-100 p-3 text-indigo-900">1fr</div>
<div class="rounded-lg bg-indigo-600 p-3 text-white">120px</div>
<div class="rounded-lg bg-indigo-100 p-3 text-indigo-900">2fr</div>
</div>
</div>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:
<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:
<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:
<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:
<!-- Font size -->
<div class="text-(length:--my-var)">…</div>
<!-- Colour -->
<div class="text-(color:--my-var)">…</div>The bracket form takes the same hint:
<div class="text-[color:var(--my-var)]">…</div>
<div class="bg-[length:200px_100px]">…</div>
<div class="bg-[image:var(--my-gradient)]">…</div>| Hint | Forces 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:
<!-- 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:
<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.
<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.
<!-- The smell: the same magic number in three places -->
<header class="h-[68px]">…</header>
<aside class="top-[68px]">…</aside>
<main class="pt-[68px]">…</main>@theme {
--spacing-header: 68px;
}<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.
| Situation | Use |
|---|---|
| A value used once, in one component | Arbitrary value |
| A value used two or more times | A @theme token |
| A value your design system already names | The existing utility |
| A property with no utility, used once | Arbitrary 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
@themewill findbg-[#bada55]. - They do not respond to theming. A hard-coded hex does not change in dark mode;
bg-surfacedoes. - 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 toshadow-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. Writew-[calc(100%_-_2rem)]. - Losing the escape in JSX. Use
String.rawfor backslash escapes inside template literals. - Omitting a type hint with a variable.
text-(--my-var)is ambiguous; usetext-(color:--my-var)ortext-(length:--my-var). - Reaching for arbitrary values before checking the scale.
p-[16px]isp-4;text-[16px]istext-base. - Using them for anything themed. Colours, radii and spacing almost always belong in
@theme.
Related topics#
- Theme & Design Tokens — where repeated values belong
- Custom Utilities — promoting an arbitrary property to a real utility
- Custom Variants — promoting an arbitrary variant
- Production Builds — why dynamic class names go missing
- Troubleshooting — when a class produces no CSS