arbitrary variants

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on arbitrary values — where arbitrary values ([17rem], [color:var(--x)]) go INSIDE a utility, arbitrary variants go BEFORE the colon ([&:nth-child(3)]:). Same brackets, different slot.

arbitrary variants — write a CSS selector in your class name

Every Tailwind variant is sugar over one idea: append a selector to the element. hover: appends :hover, first: appends :first-child. Arbitrary variants expose that mechanism raw: write the selector yourself in square brackets, use & to mark where the element goes, and Tailwind splices it into the compiled rule. No @custom-variant, no config — it works straight from HTML.

you writeTailwind compilesselector family
[&:nth-child(3)]:text-cyan-400 .el:nth-child(3) { color:… } pseudo-class
[&::-webkit-slider-thumb]:bg-cyan-500 .el::-webkit-slider-thumb { … } pseudo-element
[&[aria-pressed=true]]:bg-cyan-600 .el[aria-pressed=true] { … } attribute selector
[&_.inner]:text-cyan-400 .el .inner { … } descendant combinator
[&>div:first-child]:mt-0 .el > div:first-child { … } child + structural
[@supports(display:grid)]:grid @supports (display:grid) { .el { display:grid } } feature query (at-rule)
[@media(print)]:hidden @media print { .el { display:none } } media query (at-rule)

The & is a placeholder. It means "the element wearing this class" — the same & native CSS nesting and Sass use. Put it anywhere: [&:hover] (after), [.parent_&] (before), [&_&] (nested). For at-rules (@supports, @media) the whole condition lives inside the brackets and & is optional — Tailwind knows where the element goes.

1 · [&:nth-child(3)]: — pseudo-class on the fly

The list below carries one arbitrary variant: the 3rd item has [&:nth-child(3)]:text-cyan-400. That is the ONLY rule deciding its color — no nth-3: shorthand, no JS. Click any item to read its computed color and confirm only #3 is cyan. This is the gold-checked assertion.

  • item #1 — default ink
  • item #2 — default ink
  • item #3 — [&:nth-child(3)]:text-cyan-400
  • item #4 — default ink
  • item #5 — default ink

click an item → its computed color appears here

2 · [&::-webkit-slider-thumb]: — pseudo-element styling

The range input's thumb is a pseudo-element — ::-webkit-slider-thumb (Chromium/Safari) / ::-moz-range-thumb (Firefox). There is no built-in Tailwind variant for it, so arbitrary variants are the only clean path. Drag the slider — the cyan thumb is shaped entirely by [&::-webkit-slider-thumb]: rules.

value: 42 — thumb styled by 9 arbitrary pseudo-element variants

Two vendor prefixes, one input. Both [&::-webkit-slider-thumb]: and [&::-moz-range-thumb]: are stacked — the browser silently ignores the one it doesn't understand. This is the standard pattern for cross-browser range styling in v4.

3 · [&_.target]: — descendant combinator

The card carries [&_.target]:text-cyan-400. The &_. reads as "this element, then a space (descendant), then .target". Every descendant tagged target lights up cyan — at any depth — while un-tagged siblings stay slate. This is parent-scoped styling without group-* or a component boundary.

card root (no target class) — stays slate

paragraph.target — turns cyan

wrapper (no class) ← nested span.target at depth 2 — also cyan
  • plain li — slate
  • li.target — cyan (the variant reaches into list items too)

.target color: — · sibling color: —

4 · [@supports(display:grid)]: — feature query variant

The box carries TWO complementary at-rule variants: [@supports(display:grid)]:grid (apply if grid is supported) and [@supports(not(display:grid))]:block (fallback). Every modern browser supports grid, so display resolves to grid — proving the variant compiled into a real @supports block. The grid layout is the proof: the 3 cells sit on one row. not-supports-[display:grid]: is the shorthand alias Tailwind ships for the negation.

cell A · grid track 1
cell B · grid track 2
cell C · grid track 3

box display: — · grid-template-columns: —

stacking arbitrary + built-in variants

Arbitrary variants compose with every other variant — just chain them left-to-right, outermost first. Order matters the same way it does for built-ins: each variant wraps the next.

stacked classcompiles towhen it fires
md:[&:nth-child(3)]:bg-cyan-900 @media (min-width:48rem) { .el:nth-child(3) {…} } 3rd child, only at ≥ 768px viewport
hover:[&~.next]:text-cyan-400 .el:hover ~ .next {…} (hover sets it, sibling reads it) when this element is hovered (advanced — usually use peer-*)
dark:[&::-webkit-scrollbar]:w-2 @media (prefers-color-scheme:dark) { .el::-webkit-scrollbar {…} } dark mode + webkit scrollbar pseudo-element
[@media(print)]:[&:first-child]:hidden @media print { .el:first-child { display:none } } print media + first child — at-rules stack too
<!-- nth-child only fires at the md breakpoint --> <li class="md:[&amp;:nth-child(3)]:bg-cyan-900">3rd row, ≥ 768px</li> <!-- webkit scrollbar, only in dark mode --> <div class="dark:[&amp;::-webkit-scrollbar]:w-2 dark:[&amp;::-webkit-scrollbar-thumb]:bg-cyan-500"> scroll me </div> <!-- print: hide the first child of this section --> <section class="[@media(print)]:[&amp;:first-child]:hidden">…</section>

intent → arbitrary variant

intentpatternwhy arbitrary
style the Nth child (no shorthand) [&:nth-child(3)]: v4 ships nth-3: for simple cases; arbitrary covers nth-child(2n+1) etc.
style a vendor pseudo-element [&::-webkit-slider-thumb]: no built-in variant exists for slider thumbs / scrollbars
react to a data/aria attribute value [&[aria-pressed=true]]: aria-pressed: only matches "true"; arbitrary matches any value/attr
descendant of this element [&_.inner]: scoped styling without group-* markers
direct-child selector [&>div]: reaches only immediate children, not all descendants
feature detection [@supports(display:grid)]: progressive enhancement; supports-[…]: is the alias
print / media-only styles [@media(print)]: print: covers print; arbitrary covers any @media expression
ancestor-scoped (element is INSIDE .x) [.x_&]: compiles to .x .el — style this element only when nested in .x