Tailwind CSS Handbook v4.3

States & Variants

Style hover, focus, disabled, validation and structural states directly in your markup — plus group-*, peer-*, has-* and not-* for styling across elements.

A variant is a prefix that scopes a utility to a condition. hover:bg-blue-600 compiles to a :hover rule; md:flex compiles to a media query. Everything Tailwind can do beyond a flat style is a variant.

Overview#

Loading preview…

Variants stack, and they read left to right: dark:md:hover:bg-gray-700 means on hover, at the medium breakpoint, in dark mode.

The states you will use most#

VariantApplies whenTypical use
hover:Pointer is over the elementColour and elevation changes
focus:Element has focusInputs
focus-visible:Focus should be shownButtons and links
focus-within:A descendant has focusHighlighting a field wrapper
active:Being pressedTactile feedback
disabled:disabled attribute presentDimming, cursor
checked:Checkbox/radio is checkedToggles
visited:Link already visitedLong content

group-* and peer-*#

These are the two highest-leverage variants in Tailwind: they let a child react to a different element's state.

group-* — react to an ancestor#

Mark the parent group, then use group-hover:, group-focus:, group-has-* and friends on descendants.

Loading preview…

Name groups when they nest:

HTML
<div class="group/row">
  <div class="group/cell">
    <!-- Reacts to the row, not the cell -->
    <button class="opacity-0 group-hover/row:opacity-100">Edit row</button>
    <!-- Reacts to the cell only -->
    <span class="hidden group-hover/cell:inline">Cell hint</span>
  </div>
</div>

Without a name, group-hover: matches the nearest group ancestor, which is rarely what you want once groups nest.

peer-* — react to a previous sibling#

Loading preview…

has-* — style a parent from its children#

HTML
<label class="rounded-lg border p-4 has-checked:border-indigo-500 has-checked:bg-indigo-50">
  <input type="radio" name="plan" class="accent-indigo-600" />
  Pro plan
</label>

Combine with group:

HTML
<div class="group">
  <div class="group-has-[a]:mt-4">…</div>
</div>

not-* — invert a condition#

v4

HTML
<!-- Underline links except when hovered -->
<a class="underline not-hover:decoration-dotted" href="#">…</a>

<!-- Fallback for browsers without a feature -->
<div class="not-supports-[backdrop-filter]:bg-white">…</div>

Structural and child variants#

VariantSelector
first: / last: / only::first-child / :last-child / :only-child
odd: / even::nth-child(odd) / :nth-child(even)
nth-3: / nth-last-2::nth-child(3) / :nth-last-child(2)
empty::empty
*:Direct children — :is(& > *)
**:All descendants — :is(& *)
HTML
<ul class="*:border-b *:py-2 last:*:border-b-0">
  <li>First</li>
  <li>Second</li>
</ul>

Form validation states#

VariantApplies when
required: / optional:Field is required or not
valid: / invalid:Value passes/fails constraints
user-valid: / user-invalid:Same, but only after the user has interacted v4.1
in-range: / out-of-range:Numeric input within min/max
placeholder-shown:Input is empty and showing its placeholder
autofill:Browser autofilled the field
read-only:readonly attribute present

Pseudo-elements#

VariantElement
before: / after:::before / ::after — need content-*
placeholder:Input placeholder text
selection:Highlighted text
marker:List bullets and numbers
file:File input's button
first-letter: / first-line:Typographic flourishes
backdrop:<dialog> backdrop
HTML
<input type="file" class="file:mr-4 file:rounded-full file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100" />

Media and preference variants#

VariantCondition
dark:prefers-color-scheme: dark — see Dark Mode
motion-reduce: / motion-safe:prefers-reduced-motion
contrast-more: / contrast-less:prefers-contrast
forced-colors:Windows High Contrast mode
pointer-coarse: / pointer-fine:Touch vs mouse v4.1
noscript:JavaScript disabled v4.1
print:Printing
supports-[…]:@supports feature query
HTML
<!-- Bigger tap targets on touch devices -->
<button class="p-2 pointer-coarse:p-3">…</button>

Attribute variants#

HTML
<!-- ARIA -->
<div aria-expanded="true" class="aria-expanded:rotate-180">…</div>

<!-- Data attributes -->
<div data-state="open" class="data-[state=open]:block hidden">…</div>

<!-- Text direction -->
<span class="ltr:ml-2 rtl:mr-2">…</span>

Advanced example: an accessible dropdown#

Loading preview…

group-open:rotate-180 reads the <details> element's own open state — no JavaScript.

Common mistakes#

  • Forgetting transition. hover:bg-indigo-500 snaps unless the base state has transition.
  • peer after the target. Sibling selectors only look forward.
  • Missing content-[''] on before:/after:. Without content the pseudo-element does not render. Tailwind sets content: "" by default for these variants, but an explicit content-[''] is clearer when you also set other properties.
  • group on the wrong element. It must be on an ancestor of everything using group-*.
  • Overusing invalid:. Use user-invalid:.