Tailwind CSS Handbook v4.3

Variant Reference

Every Tailwind v4 variant and the exact selector or at-rule it generates — pseudo-classes, form states, group and peer, responsive, container and media queries.

A variant is a prefix that wraps a utility in a condition. This page lists every built-in variant with the CSS it compiles to. & is the element the utility is on; … is the value you supply.

Pseudo-classes#

VariantGenerated CSS
hover@media (hover: hover) { &:hover }
focus&:focus
focus-within&:focus-within
focus-visible&:focus-visible
active&:active
visited&:visited
target&:target

Structural#

VariantGenerated CSS
first&:first-child
last&:last-child
only&:only-child
odd&:nth-child(odd)
even&:nth-child(even)
first-of-type&:first-of-type
last-of-type&:last-of-type
only-of-type&:only-of-type
nth-<number> / nth-[…]&:nth-child(…)
nth-last-<number> / nth-last-[…]&:nth-last-child(…)
nth-of-type-<number> / nth-of-type-[…]&:nth-of-type(…)
nth-last-of-type-<number> / nth-last-of-type-[…]&:nth-last-of-type(…)
empty&:empty

Form states#

VariantGenerated CSS
disabled&:disabled
enabled&:enabled
checked&:checked
indeterminate&:indeterminate
default&:default
required&:required
optional&:optional
valid&:valid
invalid&:invalid
user-valid&:user-valid
user-invalid&:user-invalid
in-range&:in-range
out-of-range&:out-of-range
placeholder-shown&:placeholder-shown
autofill&:autofill
read-only&:read-only

Relationship variants#

These style one element based on another element's state.

VariantGenerated CSS
group-*&:is(:where(.group)… *)
group-*/nameSame, scoped to .group\/name
peer-*&:is(:where(.peer)… ~ *)
peer-*/nameSame, scoped to .peer\/name
has-[…]&:has(…)
not-[…]&:not(…)
in-[…]:where(…) &
*:is(& > *)
**:is(& *)
inert&:is([inert], [inert] *)
details-content&::details-content
open&:is([open], :popover-open, :open)

group-* and peer-* compose with any other variant: group-hover:, peer-checked:, group-has-checked:, peer-focus-visible:.

HTML
<!-- Ancestor state -->
<a href="#" class="group">
  <span class="text-gray-500 group-hover:text-indigo-600">Continue</span>
</a>

<!-- Earlier sibling state -->
<input type="email" class="peer" required />
<p class="hidden peer-user-invalid:block">Enter a valid email.</p>

<!-- Child state -->
<label class="border has-checked:border-indigo-500">
  <input type="radio" name="plan" />
</label>

not-*#

not-* inverts any other variant, including media and feature queries.

ExampleGenerated CSS
not-hover:&:not(*:hover)
not-supports-[display:grid]:@supports not (display: grid)
not-first:&:not(:first-child)

Pseudo-elements#

VariantGenerated CSS
before&::before
after&::after
first-letter&::first-letter
first-line&::first-line
marker&::marker, & *::marker
selection&::selection
file&::file-selector-button
backdrop&::backdrop
placeholder&::placeholder

Responsive#

Breakpoints come from the --breakpoint-* namespace and are min-width by default.

VariantGenerated CSS
sm@media (width >= 40rem)
md@media (width >= 48rem)
lg@media (width >= 64rem)
xl@media (width >= 80rem)
2xl@media (width >= 96rem)
max-sm@media (width < 40rem)
max-md@media (width < 48rem)
max-lg@media (width < 64rem)
max-xl@media (width < 80rem)
max-2xl@media (width < 96rem)
min-[…]@media (width >= …)
max-[…]@media (width < …)

Container queries#

Mark an ancestor @container, then query that ancestor's width instead of the viewport. Sizes come from the --container-* namespace.

VariantGenerated CSS
@3xs@container (width >= 16rem)
@2xs@container (width >= 18rem)
@xs@container (width >= 20rem)
@sm@container (width >= 24rem)
@md@container (width >= 28rem)
@lg@container (width >= 32rem)
@xl@container (width >= 36rem)
@2xl@container (width >= 42rem)
@3xl@container (width >= 48rem)
@4xl@container (width >= 56rem)
@5xl@container (width >= 64rem)
@6xl@container (width >= 72rem)
@7xl@container (width >= 80rem)
@max-3xs … @max-7xl@container (width < …) — same scale, inverted
@min-[…]@container (width >= …)
@max-[…]@container (width < …)

Named containers disambiguate nesting:

HTML
<div class="@container/card">
  <div class="@container/media">
    <p class="@lg/card:text-lg">Sized against the card, not the media block.</p>
  </div>
</div>

Media & preference#

VariantGenerated CSS
dark@media (prefers-color-scheme: dark)
motion-safe@media (prefers-reduced-motion: no-preference)
motion-reduce@media (prefers-reduced-motion: reduce)
contrast-more@media (prefers-contrast: more)
contrast-less@media (prefers-contrast: less)
forced-colors@media (forced-colors: active)
not-forced-colors@media not (forced-colors: active)
inverted-colors@media (inverted-colors: inverted)
pointer-fine@media (pointer: fine)
pointer-coarse@media (pointer: coarse)
pointer-none@media (pointer: none)
any-pointer-fine@media (any-pointer: fine)
any-pointer-coarse@media (any-pointer: coarse)
any-pointer-none@media (any-pointer: none)
portrait@media (orientation: portrait)
landscape@media (orientation: landscape)
noscript@media (scripting: none)
print@media print

Feature queries#

VariantGenerated CSS
supports-[…]@supports (…)
not-supports-[…]@supports not (…)
HTML
<div class="bg-white/90 supports-[backdrop-filter]:bg-white/60 supports-[backdrop-filter]:backdrop-blur-md">…</div>

A bare property name tests for its existence: supports-[container-type]:. A full declaration tests for a value: supports-[display:grid]:. Use _ for spaces.

Attribute variants#

VariantGenerated CSS
aria-busy&[aria-busy="true"]
aria-checked&[aria-checked="true"]
aria-disabled&[aria-disabled="true"]
aria-expanded&[aria-expanded="true"]
aria-hidden&[aria-hidden="true"]
aria-pressed&[aria-pressed="true"]
aria-readonly&[aria-readonly="true"]
aria-required&[aria-required="true"]
aria-selected&[aria-selected="true"]
aria-[…]&[aria-…]
data-[…]&[data-…]
rtl&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)
ltr&:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)
HTML
<button aria-expanded="false" class="aria-expanded:rotate-180">…</button>
<div data-state="open" class="hidden data-[state=open]:block">…</div>

starting#

v4

VariantGenerated CSS
starting@starting-style

starting: supplies the value an element animates from the first time it is rendered — including elements moving out of display: none.

HTML
<div popover class="opacity-100 transition-discrete transition-opacity starting:open:opacity-0">
  Fades in on first paint.
</div>

Stacking variants#

Variants stack by chaining them with :. They read left to right as conditions, all of which must hold.

HTML
<button class="dark:md:hover:bg-fuchsia-600">…</button>

That is: in dark mode, at md and above, on hover.

Loading preview…

Rules worth knowing:

  • Order does not change the output for independent conditions. md:hover: and hover:md: generate the same rule. Pick one order and keep it consistent — the docs order is media query, then state.
  • **Order does matter for group-*, peer-* and not-*, because they change what the rest of the selector attaches to. group-hover:focus: means "the group is hovered and** this element is focused".
  • Tailwind sorts the output, not your class attribute. Utilities are emitted in a fixed cascade order, so text-sm md:text-lg works regardless of which you write first. Two utilities setting the same property at the same variant level resolve by that internal order, not by source order — do not rely on p-2 p-4.
  • Stacked variants nest their at-rules. dark:md: produces a @media (prefers-color-scheme: dark) block containing a @media (width >= 48rem) block.
  • Arbitrary variants stack too. [&_p]:hover:underline is valid; & marks the element and _ stands in for a space.

Arbitrary variants#

When no built-in variant fits, write the selector inline:

ExampleGenerated CSS
[&.is-dragging]:cursor-grabbing&.is-dragging
[&>*]:p-4& > *
[&_p]:mt-4& p
[@supports(display:grid)]:grid@supports (display: grid)
[@media(min-resolution:2dppx)]:hiddenThat media query

If the same arbitrary variant appears more than twice, promote it with @custom-variant.

Common mistakes#

  • Putting peer after the element that reacts to it. Sibling combinators only look forward.
  • Using invalid: for validation feedback. Use user-invalid: so untouched fields stay neutral.
  • Expecting group-hover: to work without group. The marker class must be on an ancestor.
  • Forgetting transition on the base state. Variants change the value; only transition makes the change animate.
  • Overriding a *: rule on the child. Same specificity, later in the output — the parent wins.
  • Removing focus styles with outline-none. Use outline-hidden, and add a focus-visible: ring.