arbitrary properties

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Sibling concept: arbitrary values (custom VALUE for an existing utility, e.g. w-[17rem]). This page covers arbitrary PROPERTIES — setting a CSS property that has NO utility.

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.

aspectarbitrary 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.

h-[var(--card-height)]
60% of it via calc()

--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.

TAILWIND V4 · VERTICAL

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.)

slide 1 — scroll ↓
slide 2 — snaps here
slide 3 — end

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

intentpatterncompiled 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