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#
| Variant | Generated CSS |
|---|---|
hover | @media (hover: hover) { &:hover } |
focus | &:focus |
focus-within | &:focus-within |
focus-visible | &:focus-visible |
active | &:active |
visited | &:visited |
target | &:target |
Structural#
| Variant | Generated 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#
| Variant | Generated 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.
| Variant | Generated CSS |
|---|---|
group-* | &:is(:where(.group)… *) |
group-*/name | Same, scoped to .group\/name |
peer-* | &:is(:where(.peer)… ~ *) |
peer-*/name | Same, 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:.
<!-- 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.
| Example | Generated CSS |
|---|---|
not-hover: | &:not(*:hover) |
not-supports-[display:grid]: | @supports not (display: grid) |
not-first: | &:not(:first-child) |
Pseudo-elements#
| Variant | Generated 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.
| Variant | Generated 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.
| Variant | Generated 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:
<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#
| Variant | Generated 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#
| Variant | Generated CSS |
|---|---|
supports-[…] | @supports (…) |
not-supports-[…] | @supports not (…) |
<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#
| Variant | Generated 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"] *) |
<button aria-expanded="false" class="aria-expanded:rotate-180">…</button>
<div data-state="open" class="hidden data-[state=open]:block">…</div>starting#
v4
| Variant | Generated 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.
<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.
<button class="dark:md:hover:bg-fuchsia-600">…</button>That is: in dark mode, at md and above, on hover.
<div class="p-6">
<ul class="mx-auto max-w-xs divide-y divide-gray-200 rounded-lg border border-gray-200">
<li class="px-4 py-2 text-sm first:rounded-t-lg last:rounded-b-lg odd:bg-gray-50 hover:bg-indigo-50">Alpha</li>
<li class="px-4 py-2 text-sm first:rounded-t-lg last:rounded-b-lg odd:bg-gray-50 hover:bg-indigo-50">Beta</li>
<li class="px-4 py-2 text-sm first:rounded-t-lg last:rounded-b-lg odd:bg-gray-50 hover:bg-indigo-50">Gamma</li>
</ul>
</div>Rules worth knowing:
- Order does not change the output for independent conditions.
md:hover:andhover: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-*andnot-*, 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-lgworks 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 onp-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:underlineis valid;&marks the element and_stands in for a space.
Arbitrary variants#
When no built-in variant fits, write the selector inline:
| Example | Generated CSS |
|---|---|
[&.is-dragging]:cursor-grabbing | &.is-dragging |
[&>*]:p-4 | & > * |
[&_p]:mt-4 | & p |
[@supports(display:grid)]:grid | @supports (display: grid) |
[@media(min-resolution:2dppx)]:hidden | That media query |
If the same arbitrary variant appears more than twice, promote it with @custom-variant.
Common mistakes#
- Putting
peerafter the element that reacts to it. Sibling combinators only look forward. - Using
invalid:for validation feedback. Useuser-invalid:so untouched fields stay neutral. - Expecting
group-hover:to work withoutgroup. The marker class must be on an ancestor. - Forgetting
transitionon the base state. Variants change the value; onlytransitionmakes the change animate. - Overriding a
*:rule on the child. Same specificity, later in the output — the parent wins. - Removing focus styles with
outline-none. Useoutline-hidden, and add afocus-visible:ring.
Related topics#
- States & Variants — the same material with worked examples
- Custom Variants —
@custom-variantand@variant - Responsive Design — breakpoints in practice
- Container Queries —
@containerin depth - Cheat Sheet — the utilities these variants wrap