Hover, Focus, and Other States

Purpose and Scope

This page explains Tailwind CSS state variants: prefixes such as hover:, focus:, dark:, disabled:, and responsive or attribute-driven forms that make a utility apply only under a condition. A utility is a single-purpose class like bg-sky-500; a variant is the condition placed before it, producing a candidate like hover:bg-sky-700. The important mental model is that Tailwind does not mutate the meaning of one semantic class. Instead, each conditional rule is represented as its own complete class candidate in your markup, giving the compiler an explicit token to discover and turn into CSS.

Sources: packages/tailwindcss/src/index.ts, packages/tailwindcss/package.json

Variants cover more than hover and focus. Official documentation groups them around pseudo-classes, pseudo-elements, media and feature queries, attribute selectors, and child selectors. That means the same composition model can represent focus-visible:outline-2, before:content-[''], motion-safe:transition, rtl:space-x-reverse, or group and peer state patterns. These are not separate styling systems; they are conditional wrappers around normal utilities. Once you understand that the utility remains the payload and the variant changes the selector or at-rule context, stacked state expressions become easier to read and debug.

Sources: packages/tailwindcss/src/index.ts

Relevant Source Files

  • packages/tailwindcss/src/index.ts — Core compiler entry point that imports candidate compilation, the design system, CSS parsing, and variant substitution support. It also exposes feature flags that include Variants for @variant usage.
  • packages/tailwindcss/package.json — Public package metadata and exports for the tailwindcss package, including the main CSS entry points and TypeScript source export used by integrations during development.
  • package.json — Root workspace scripts for building, testing, integration testing, UI testing, and package development across the Tailwind CSS monorepo.
  • packages/@tailwindcss-browser/src/index.ts — Browser runtime that creates a Tailwind compiler from style[type="text/tailwindcss"] blocks, injects the main import when needed, tracks seen classes, and recompiles when stylesheets change.
  • packages/@tailwindcss-cli/src/index.ts — CLI entry point that routes tailwindcss, tailwindcss build, and tailwindcss canonicalize commands into build and candidate-normalization workflows.
  • packages/@tailwindcss-node/src/index.ts — Node package surface that re-exports compile, optimization, source-map, instrumentation, and path helpers used by server-side integrations.

Variant Composition Model

In practical markup, state variants are written left to right before the utility name. For example, hover:bg-sky-700 says “generate the bg-sky-700 background-color utility, but scope it to the hover state.” Stacking variants narrows the condition further: dark:hover:bg-slate-700 means the hover state inside the dark-mode condition. The class must still appear as a complete token in your source, because Tailwind’s documentation describes class detection as plain-text scanning rather than language-aware evaluation. Avoid constructing pieces like hover:bg-${color}-700; write complete alternatives instead so every desired candidate can be found.

Sources: packages/tailwindcss/src/index.ts

<button class="bg-sky-500 hover:bg-sky-700 focus:outline-2 focus:outline-sky-500 disabled:opacity-50">
  Save changes
</button>

The compiler-side evidence shows why this uniform syntax matters. The core package imports compileCandidates, buildDesignSystem, and substituteAtVariant, connecting discovered candidates, theme-aware utility generation, and variant transformation in one compilation path. The Features enum also includes a Variants bit for @variant, meaning variant behavior is part of the feature model the compiler can detect while parsing CSS. From a user perspective, classes in HTML and variant directives in CSS are separate authoring surfaces, but both are handled by the same package that owns utility and variant compilation.

Sources: packages/tailwindcss/src/index.ts

System-to-Code Mapping

Reader conceptSource-backed implementation anchorWhat it means for state styling
Utility candidatecompileCandidates import in packages/tailwindcss/src/index.tsA class-like token is compiled into CSS only if Tailwind recognizes its utility and modifiers.
Variant transformationsubstituteAtVariant import in packages/tailwindcss/src/index.tsConditional contexts such as pseudo-classes or media wrappers are applied around utilities or custom variant blocks.
CSS authoring surfaceFeatures.Variants in packages/tailwindcss/src/index.ts@variant usage is tracked as a compiler feature, not treated as an unrelated post-processing step.
Published packagepackages/tailwindcss/package.json exportsConsumers import tailwindcss, tailwindcss/index.css, tailwindcss/theme.css, tailwindcss/preflight.css, and tailwindcss/utilities.css through documented package entry points.
Browser runtimepackages/@tailwindcss-browser/src/index.tsPlay/CDN-style usage creates a compiler in the page and builds CSS from classes discovered over time.
CLI runtimepackages/@tailwindcss-cli/src/index.tsLocal builds can be run through tailwindcss build, while canonicalize supports candidate-list normalization workflows.
Node runtimepackages/@tailwindcss-node/src/index.tsFramework integrations get compile, optimize, source-map, path, and instrumentation exports from a Node-oriented package.

Conditional utilities are therefore not an isolated documentation feature. They travel through the same surfaces as every other utility: package exports define how the compiler and CSS assets are consumed, runtime packages decide where compilation happens, and the core compiler interprets candidates and variant forms. The browser runtime is especially useful for understanding interactive demos: it creates the compiler from Tailwind CSS input styles, adds @import "tailwindcss" if the page has not provided imports, and maintains a set of seen classes so subsequent builds can focus on new candidates.

Sources: packages/tailwindcss/package.json, packages/@tailwindcss-browser/src/index.ts

Execution Flow Across Runtimes

In a framework or build-tool setup, your templates contain complete classes like hover:text-white and focus-visible:ring-2. The integration that wraps Tailwind feeds CSS and discovered class candidates into the compiler; the core package then emits rules whose selectors or at-rules encode the variant condition. In a CLI workflow, packages/@tailwindcss-cli/src/index.ts shows the command router for tailwindcss build, help output, and canonicalize. That gives contributors and tooling authors a concrete entry point for reproducing variant output outside a framework.

Sources: packages/@tailwindcss-cli/src/index.ts, packages/tailwindcss/src/index.ts

In the browser package, the flow is deliberately different because there is no Node build step. packages/@tailwindcss-browser/src/index.ts creates a <style> element for generated output, reads all style[type="text/tailwindcss"] input blocks, resolves virtual Tailwind CSS assets such as the index, preflight, theme, and utilities stylesheets, and constructs a compiler with tailwindcss.compile. It also tracks classes already seen on the page. That model supports quick browser/CDN experimentation with the same state-variant syntax, while keeping the compiler lifecycle visible in the runtime source.

Sources: packages/@tailwindcss-browser/src/index.ts

Node integrations sit between those two modes. The @tailwindcss/node entry point re-exports compile, instrumentation, normalization, optimization, and source-map modules, and registers an ESM cache hook when supported by the current Node runtime. For state variants, the important point is not that Node has a separate selector engine; it provides the build environment that calls into the shared Tailwind compiler. Vite, PostCSS, Webpack, and other Node-based integrations can therefore share the same interpretation of hover:, focus:, media, and attribute variants.

Sources: packages/@tailwindcss-node/src/index.ts

Authoring Guidance and Examples

Use variants when the condition belongs to the element’s state, environment, relationship, or generated selector rather than to a separate component abstraction. A disabled button can combine default, hover, focus, and disabled utilities without introducing a .button stylesheet rule. A disclosure component can use an attribute or open-state variant when the state is already expressed in markup. A themed component can stack dark mode with interaction states. The result is verbose at first glance, but each class remains independently removable, searchable, and testable because it is a complete utility candidate.

Sources: packages/tailwindcss/src/index.ts

<details class="group rounded-lg border p-4">
  <summary class="cursor-pointer font-medium group-open:text-sky-600">
    Account settings
  </summary>
  <div class="mt-2 text-sm text-slate-600 dark:text-slate-300 group-open:block">
    Manage profile, security, and notification preferences.
  </div>
</details>

When debugging a missing state rule, start with the class token, not the generated CSS. Confirm that the full class string exists in source text, including every variant prefix and arbitrary value delimiter. Then confirm that the environment you are using actually runs Tailwind: the CLI must reach the build handler, the browser runtime must have a Tailwind input style block or injected import, and Node integrations must call the exported compile path. If the same complete class works in one runtime but not another, compare the integration setup before assuming the variant name is unsupported.

Sources: packages/@tailwindcss-cli/src/index.ts, packages/@tailwindcss-browser/src/index.ts, packages/@tailwindcss-node/src/index.ts

Testing and Operational Signals

The root workspace exposes the operational commands that keep state variant behavior aligned across packages: pnpm test runs Rust and Vitest suites, pnpm test:integrations targets integration tests, and pnpm test:ui delegates UI tests to the core and browser packages. Those scripts matter for state variants because selector generation must be consistent whether classes are compiled through the core package, exercised in browser demos, or routed through integration packages. The package-level tailwindcss metadata also includes test:ui, showing that browser-observable behavior is treated as part of the core package validation story.

Sources: package.json, packages/tailwindcss/package.json

Next, read styling-with-utility-classes for the general utility-first model, responsive-design for breakpoint variants, dark-mode for color-scheme state, and configuration-and-plugin-api for defining custom variants and plugin-powered styling surfaces.