Capítulo 11 de 29

Chapter 11: Hover, Focus & Other States (Variants)

Core Idea

Every interactive/structural/conditional state in Tailwind is a class prefix ("variant") stacked directly on a utility — hover:bg-sky-700, dark:lg:has-checked:bg-indigo-50 — covering pseudo-classes, pseudo-elements, media/feature queries, attribute selectors, and parent/sibling/descendant relationships, all composable in one system instead of scattered across media queries, :hover blocks, and JS state.

Key Concepts

Pseudo-classes

  • Interaction: hover:, focus:, active:, plus visited:, focus-within:, focus-visible:.
  • Structural position: first:, last:, only:, odd:, even:, first-of-type:, last-of-type:, only-of-type:, empty:. nth-<n>:/nth-last-<n>:/nth-of-type-<n>:/nth-last-of-type-<n>: take a number directly (nth-3:underline) or an arbitrary expression (nth-[2n+1_of_li]).
  • Form state: required:, invalid:, valid:, disabled:, enabled:, read-only:, indeterminate:, checked:, default:, optional:, placeholder-shown:, autofill:, in-range:, out-of-range:. Styling these as variants (instead of branching class strings in JS per form state) keeps one class list that works regardless of the input's current state.
  • :has()has-*: styles an element based on its descendantshas-checked:bg-indigo-50 on a <label> wrapping a radio input styles the label once that input is checked. Combine with a pseudo-class (has-[:focus]) or an element selector (has-[img], has-[a]) arbitrarily.
  • :not()not-*: styles when a condition is false — e.g. hover:not-focus:bg-indigo-700 applies hover styles only while not focused. Combines with media/feature variants too (not-supports-[display:grid]:flex).
  • Parent state → group + group-*: mark an ancestor group (name it group/{name} when nested groups need disambiguation, target with group-hover/{name}:), then style any descendant with group-hover:, group-focus:, group-has-checked:, group-has-[a]:, etc.
  • Sibling state → peer + peer-*: mark a sibling peer (nameable the same way, peer/{name}), then style a later sibling with peer-checked:, peer-has-checked:, etc. — CSS's ~ general sibling combinator only looks forward, so the peer element must come before the styled element in markup.

Pseudo-elements

  • before:/after: — require setting content-[''] (or a real string) to render; commonly paired with utilities for size/position/color to draw decorative marks.
  • placeholder: — styles ::placeholder text.
  • file: — styles the button part of <input type="file">.
  • marker: — styles list bullets/numbers (::marker).
  • selection: — styles user text-selection highlight color.
  • first-line:/first-letter: — styles ::first-line/::first-letter.
  • backdrop: — styles the ::backdrop behind a native <dialog>/fullscreen element.

Media & Feature Queries

  • Responsive breakpoints (sm:2xl:) and container queries (@sm:…) are variants too — see the Responsive Design and Container Queries chapters.
  • dark:prefers-color-scheme: dark (overridable, see Dark Mode chapter).
  • motion-reduce:/motion-safe:prefers-reduced-motion.
  • contrast-more:/contrast-less:prefers-contrast.
  • forced-colors: — Windows High Contrast / forced-colors mode; pairs with forced-color-adjust-* utilities to opt specific elements in/out.
  • inverted-colors: — OS-level color inversion.
  • pointer-fine:/pointer-coarse:/pointer-none: and any-pointer-* variants — primary vs. any available pointer precision (mouse vs. touch).
  • portrait:/landscape: — device/viewport orientation.
  • noscript:(scripting: none), i.e. JS disabled.
  • print: — print media.
  • supports-[...]: — arbitrary @supports feature-query variant, e.g. supports-[display:grid]:grid.
  • starting:@starting-style, for entry-transition starting values (animating an element in on first paint/mount, no JS needed).

Attribute Selectors

  • ARIA state variants: aria-checked:, aria-disabled:, aria-expanded:, aria-hidden:, aria-pressed:, aria-selected:, and more — target aria-* attributes set by JS UI libraries directly, plus aria-[<name>=<value>] for arbitrary ARIA attribute/value pairs.
  • Data attribute variants: data-[<name>]:/data-[<name>=<value>]: — styles based on a custom data-* attribute (common with headless UI libraries that expose state via data-state, data-open, etc.).
  • RTL support: rtl:/ltr: — style differently based on text direction, for logical/directional overrides beyond what the ps-*/pe-*/start-*/end-* logical-property utilities already handle automatically.
  • Open/closed state: open: — targets the [open] attribute on <details>, <dialog>, and the Popover API's open state.
  • Inert elements: inert: — targets elements with the inert attribute (interaction-disabled, kept in the accessibility tree as non-interactive) — pair with utilities to visually de-emphasize inert content (e.g. dim it) to match its non-interactive state.

Child Selectors

  • *: — styles direct children (*: prefix applied to the parent, e.g. *:rounded-full rounds every direct child). Intentionally shallow — does not reach grandchildren.
  • **: — styles all descendants, not just direct children (use sparingly — broad selectors are easy to fight later).

Custom Variants

  • Arbitrary variant: [&:nth-child(-n+3)]:hover:underline-style square-bracket selector for a one-off condition with no built-in variant (see Adding Custom Styles chapter for the full arbitrary-variant syntax).
  • Registering a reusable custom variant: @custom-variant in CSS (see Adding Custom Styles / Functions and Directives chapters) — the CSS-side counterpart for anything used repeatedly enough to deserve a name.

Code Examples


<div class="dark:lg:group-has-checked:hover:bg-indigo-900">...</div>


<label class="peer ...">
  <input type="checkbox" />
</label>
<svg class="peer-has-checked:hidden ...">...</svg>
  • What it demonstrates: variants stack in any combination/order, and peer-*/group-* both require the marked element (peer/group) to be an ancestor (group) or an earlier sibling (peer) of the styled element.

Reference Tables

RelationshipMark this elementStyle with
Descendant based on ancestor stateancestor gets group (or group/{name})descendant: group-*: (or group-*/{name}:)
Element based on a later/earlier siblingsibling gets peer (or peer/{name})a later sibling: peer-*: (or peer-*/{name}:)
Element based on its own descendantshas-*:, e.g. has-checked:, has-[img]:
Direct children only*: on the parent
All descendants**: on the ancestor

Key Takeaways

  1. A variant class only ever contains styles for its own condition — never assume a class also carries a "default" state; the unprefixed utility is the default.
  2. peer-* requires the peer element to appear before the styled element in the DOM (sibling combinators only look forward) — group-* has no such ordering constraint since it targets any descendant.
  3. has-* styles an element by its own descendants' state/content; group-has-*/peer-has-* extend that same descendant-check to an ancestor's or sibling's descendants.
  4. Reach for data-[...]:/aria-[...]: variants before writing custom JS-driven class toggling when a headless UI library already exposes state via attributes.
  5. @starting-style (starting:) enables enter transitions without JS — check browser support before relying on it as the only entry animation.

Connects To

  • Responsive Design / Container Queries: breakpoint and container-size prefixes are variants using the same stacking rules as everything in this chapter.
  • Dark Mode: dark: is a variant like any other, overridable via @custom-variant.
  • Adding Custom Styles: arbitrary variants ([&:...]:) and @custom-variant registration are documented there in full.