Overview
A variant scopes a utility to a condition: hover:bg-brand applies bg-brand only on hover. Built-in variants cover pseudo-classes, parent, sibling, and descendant state, feature queries, and attributes, so most state needs no JavaScript. The hub is tailwind.
Style children from parent state with group
Mark the parent group and prefix descendants with group-hover:, group-focus:, or group-aria-expanded:. Name groups when nesting: group/card pairs with group-hover/card:.
<div class="group/card rounded-card p-4">
<h2 class="group-hover/card:text-brand">Title</h2>
</div>Style siblings from state with peer
Mark an element peer; later siblings use peer-checked:, peer-focus:, or peer-invalid:. The peer must come first in the DOM, because the CSS sibling combinator only looks forward.
<input id="t" type="checkbox" class="peer sr-only" />
<label for="t" class="peer-checked:text-brand">Notify me</label>Style parents from child state with has-*
has-[...] wraps :has(). Style a container from what it holds; see css-has-selector.
<div class="rounded-md has-[input:focus]:ring-2 has-[.error]:border-danger">
<input type="text" />
</div>Use aria-* and data-* for state
Prefer attributes over toggled classes. aria-expanded: and aria-selected: style from accessibility state, which also keeps the markup honest. data-[state=open]:block styles from custom attributes, the pattern Radix UI and Base UI expose.
<div data-state="open" class="hidden data-[state=open]:block">Panel</div>Gate enhancements with supports-*
supports-[display:grid]:grid maps to @supports. Use it for features that are not Baseline yet, such as scroll-driven animations; do not wrap features that already work everywhere.
Write arbitrary variants for one-off selectors
Use [&...] when no named variant fits. & is the element.
<ul class="[&>li]:py-2 [&>li:first-child]:pt-0">If the pattern repeats in three places, define a @custom-variant; see tailwind-plugins.
Remember that v4 stacks variants left to right
Variants apply in the order written, which matters when they rewrite the selector. v3’s first:*:pt-0 is *:first:pt-0 in v4. For media-style variants such as sm:hover:dark:bg-brand-900, order does not change the result. The Prettier plugin sorts variants; see tailwind.