Overview
Tailwind variants are prefixes that scope a utility to a CSS condition. hover:bg-brand means bg-brand only when the element is hovered. v4 ships group-*, peer-*, has-*, supports-*, and data-* as first-class variants, plus an escape hatch for any CSS selector or at-rule. The parent reference is tailwind.
Use group-* to style children based on parent state
Mark the parent with group and style descendants with group-hover:, group-focus:, group-aria-expanded:, or any other state.
<div class="group rounded-card p-4 hover:shadow-card">
<h2 class="text-ink group-hover:text-brand transition-colors">Card title</h2>
<p class="text-ink/60 group-hover:text-ink/80">Description</p>
</div>Name groups when nesting: group/outer and group/inner. Use group-hover/outer: on a deeply nested child to target the specific ancestor.
<div class="group/card">
<div class="group/section">
<span class="group-hover/card:text-brand group-hover/section:underline">
Nested label
</span>
</div>
</div>Use peer-* to style siblings based on sibling state
peer marks an element as the source; peer-checked:, peer-focus:, and peer-invalid: style a following sibling.
<input id="toggle" type="checkbox" class="peer sr-only" />
<label for="toggle" class="cursor-pointer peer-checked:text-brand">
Enable notifications
</label>Peers must precede their targets in the DOM. A peer-* variant on an element that precedes the peer source has no effect. Name peers the same way you name groups: peer/input and peer-focus/input:.
Use has-* to style parents based on child state
has-* is the CSS :has() selector wrapped as a variant. Style a container when it contains a specific element or state.
<div class="has-[input:focus]:ring-2 has-[input:focus]:ring-brand rounded-md">
<input type="text" placeholder="Search" />
</div>The outer div gains a ring when any input inside it is focused. No JavaScript state management needed. For the deeper CSS reference, see css-has-selector.
Combine with other variants: has-[.error]:border-danger styles a field group red when it contains an error message.
Use supports-* to progressively enhance for modern features
supports-* maps to CSS @supports. Apply a utility only when the browser supports a given property or value.
<div class="grid supports-[container-type:inline-size]:container-query-layout">Use supports-* sparingly. Most modern CSS features have shipped in every evergreen browser. Reach for it when you need a meaningful fallback for a genuinely experimental feature like scroll-driven animations or @starting-style.
Use data-* to style based on custom data attributes
data-* variants match data-* attributes on the element.
<button data-variant="primary" class="data-[variant=primary]:bg-brand data-[variant=primary]:text-white">
Submit
</button>Pair with JavaScript that sets data-state="open" / data-state="closed" (the pattern Radix UI and similar libraries use) to drive visible state from semantic attributes instead of className toggling.
<div data-state="open" class="hidden data-[state=open]:block">Panel content</div>Write arbitrary variants for one-off CSS selectors
Use [...] to write an arbitrary CSS selector or at-rule as a variant when no built-in variant fits.
<p class="[&:nth-child(3)]:font-bold [&>strong]:text-brand">The & is the placeholder for the element itself, matching CSS nesting syntax. Use this for pseudo-classes that Tailwind does not expose as named variants (nth-child, not, is), for attribute selectors, or for scoped overrides inside a third-party component.
Order variants consistently; stack them left to right
Multiple variants stack left to right in class name order. The rightmost element is the base utility.
<button class="sm:hover:dark:bg-brand-900">This reads: on small-and-up viewports, when hovered, in dark mode, use bg-brand-900. The Prettier plugin for Tailwind sorts variants in a canonical order that matches this mental model. Install it; do not debate order in review. See tailwind-responsive for breakpoint variant ordering.
Prefer semantic variants over JavaScript state syncing
Replace boolean class toggling in JavaScript with data attributes and data-* variants wherever possible. The result is fewer state variables, fewer re-renders in react, and a component whose visual states are readable from the HTML alone.