arbitrary VALUE vs arbitrary PROPERTY — the one distinction
Both use square brackets, both go through the JIT, both let you escape the theme — but they target different layers of CSS: arbitrary VALUE feeds a number/string into an EXISTING utility; arbitrary PROPERTY lets you set a property Tailwind has no utility for at all. Get this distinction right and the entire bracket syntax becomes predictable.
| aspect | arbitrary VALUE w-[17rem] | arbitrary PROPERTY [mask-type:luminance] |
|---|---|---|
| shape | utility-[value] |
[property:value] — note the leading [ |
| what it does | feeds a custom value into a known utility (width, padding, color) | sets a CSS property that has no Tailwind utility |
| type hint | supported: w-[length:17rem], bg-[color:var(--x)] |
not used — the part before : IS the property name |
| generated CSS | .w-\[17rem\] { width: 17rem } |
.\[mask-type\:luminance\] { mask-type: luminance } |
| typical use | one-off size, color, grid template, image URL | text-wrap, hyphens, writing-mode, scroll-snap-*, caret-color |
| set a CSS var? | read only — bg-[var(--brand)] |
yes — [--card-height:200px] writes a custom property |
Rule of thumb: if Tailwind already has a utility prefix for it (w-, p-, bg-, text-, grid-cols-…), you want an arbitrary value. If you find yourself wishing for a utility that does not exist (mask-type, text-wrap, hyphens…), you want an arbitrary property.
1 · [text-wrap:balance] — CSS text balancing, no utility exists
text-wrap: balance (Chrome 114+, Safari 17.5+, FF 121+) tells
the browser to balance a headline's line lengths instead of filling each
line greedily. Tailwind v4 ships no text-wrap-* utility, so
the arbitrary-property form is the canonical way to apply it.
default (greedy wrap)
Tailwind v4 ships balanced headlines without any plugin
[text-wrap:balance]
Tailwind v4 ships balanced headlines without any plugin
text-wrap: —
2 · [--card-height:200px] — set CSS custom properties inline
A leading -- inside the brackets is special: it writes a CSS
custom property onto the element instead of a regular property. Pair it with
an arbitrary value on a child — h-[var(--card-height)] —
and you have a one-line token system with no @theme boilerplate.
This is the cheapest way to pass a computed value down to descendants.
--card-height: — · --card-accent: —
3 · [writing-mode:vertical-rl] — vertical text layout
Rotate the inline axis 90° — used for CJK side-labels, magazine spines,
and vertical table headers. Tailwind has no writing-mode utility;
arbitrary properties are the only inline path. The text flows
top-to-bottom and lines stack right-to-left.
Without the [text-orientation:upright] override
Latin letters would rotate sideways instead of standing upright — the
default for vertical-rl is to rotate each glyph. Chain two arbitrary
properties together to get the magazine look.
writing-mode: —
4 · [hyphens:auto] — automatic word-breaking (needs lang=)
hyphens: auto lets the browser insert hyphens at linguistic
break points inside long words — but ONLY if the element (or an ancestor)
declares a lang attribute. No lang? The browser cannot pick a
hyphenation dictionary and the rule silently does nothing. A famous
"applied but invisible" trap.
no lang attribute (silent fail)
Characteristic countermeasures institutionalize extraordinary overengineering.
lang="en" + [hyphens:auto]
Characteristic countermeasures institutionalize extraordinary overengineering.
hyphens: —
5 · [scroll-snap-type:y_mandatory] — scroll container snapping
Build a horizontal carousel or vertical pager with one arbitrary property
on the container + scroll-snap-align on each child. Note the
underscore becomes a space: [scroll-snap-type:y_mandatory]
compiles to scroll-snap-type: y mandatory — the same rule as
arbitrary values. (An escaped \_ would survive as a literal
underscore if a value actually needed one.)
scroll-snap-type: —
6 · focus:[caret-color:#06b6d4] — arbitrary property + variant
Arbitrary properties stack with variants the same way every other utility
does — prefix with the variant and a colon. Below, the input's caret is the
default grey until you focus it, at which point [caret-color:#06b6d4]
fires and the cursor turns cyan.
caret-color (focused): —
intent → pattern
| intent | pattern | compiled CSS |
|---|---|---|
| set a property with no utility | [mask-type:luminance] |
mask-type: luminance |
| balance headline line lengths | [text-wrap:balance] |
text-wrap: balance |
| set a CSS custom property | [--card-height:200px] |
--card-height: 200px |
| consume that property on a child | h-[var(--card-height)] |
height: var(--card-height) |
| multi-word value (underscore → space) | [scroll-snap-type:y_mandatory] |
scroll-snap-type: y mandatory |
| literal underscore in the value | [--my-var:foo\_bar] |
--my-var: foo_bar |
| stack with a variant | focus:[caret-color:#06b6d4] |
:focus { caret-color: #06b6d4 } |
| vertical CJK / spine text | [writing-mode:vertical-rl] |
writing-mode: vertical-rl |