Open-Source Wikis

/

Tailwind CSS

/

Features

/

Variants

tailwindlabs/tailwindcss

Variants

A variant is a class prefix that wraps generated rules in a selector or at-rule. hover:bg-red-500 becomes .hover\:bg-red-500:hover { background-color: ... }. md:flex becomes @media (min-width: 768px) { .md\:flex { display: flex } }. The framework registers ~100 built-in variants and lets users register more via @custom-variant, @variant, and JavaScript plugins.

Implementation

Three files do most of the work:

File Responsibility
packages/tailwindcss/src/variants.ts The Variants class and the registration of every built-in.
packages/tailwindcss/src/compile.ts applyVariant — walks a Candidate's variant chain and mutates the rule.
packages/tailwindcss/src/candidate.ts parseCandidate — pulls variants out of the candidate string.

The Variants class

Variants (in variants.ts) is a registry keyed by variant root name. Each entry stores:

{
  kind: 'static' | 'functional' | 'compound',
  order: number,                          // Sort priority across variants
  applyFn: VariantFn,                     // Mutates the rule in place
  compoundsWith: Compounds,               // What this variant accepts inside it
  compounds: Compounds,                   // What kinds of rules this variant produces
}

Compounds is a bit-set with AtRules, StyleRules, Never. It tells the framework whether a variant can be used as the inner half of a compound variant (group-hover:, not-, etc.) and what kinds of rules it can wrap.

Three registration shapes:

  • statichover:, focus:, dark:. The applyFn takes a Rule and rewrites it in place.
  • functionaldata-[state=open]:, aria-[checked]:. Takes a parameter; the applyFn reads variant.value to construct the wrapper.
  • compound — wraps another variant. Implemented in applyVariant rather than as a separate registration kind.

Variant kinds in the AST

packages/tailwindcss/src/candidate.ts defines three Variant shapes:

type Variant =
  | { kind: 'static'; root: string; compounds: Compounds }
  | { kind: 'functional'; root: string; value: VariantValue | null; modifier: ... }
  | { kind: 'arbitrary'; selector: string; relative: boolean; compounds: Compounds }
  | { kind: 'compound'; root: string; modifier: ...; variant: Variant; compounds: Compounds }

arbitrary variants are bracket-form variants like [&_p]: or [@media(min-width:_900px)]:. compound variants are like not-, group-hover-, peer-aria-checked-, etc.

Applying variants

applyVariant(node, variant, variants, depth) in packages/tailwindcss/src/compile.ts is the dispatch point:

graph TD
    Start[applyVariant node, variant] --> Kind{variant.kind?}
    Kind -->|arbitrary| Arb[Wrap node in selector rule]
    Kind -->|compound| Inner[Recursively applyVariant inner]
    Inner --> Apply[Call outer's applyFn on each child]
    Kind -->|static or functional| Direct[Call applyFn]

Notes:

  • The not variant cannot negate sibling at-rules (would require transforming OR into AND), so when its inner produces multiple siblings the function returns null and the candidate is discarded.
  • applyVariant returns null when the variant is incompatible with the rule. The candidate then yields no CSS.
  • For compound variants, the implementation creates a placeholder @slot AtRule, applies the inner variant to it, then walks the placeholder's children and applies the outer variant to each.

Variant ordering

The output CSS sorts utilities by the variants they use. Otherwise hover:flex and md:flex could interleave in surprising ways.

Variants.compare(a, z) produces a stable order. Variants keeps a numeric order per variant root that the comparator reads. getVariantOrder() on the design system runs the comparator over every parsed variant in the input and assigns each a bit position so compileCandidates can build a bigint mask per AST node and sort by it.

The bit-mask trick: every variant gets a unique bit. A candidate's variant chain becomes a single bigint with one bit per variant. Comparing two candidates' bigints compares the union of variants used.

Built-in variants registered in variants.ts

By family, the registrations are:

  • Pseudo classeshover, focus, active, visited, target, first, last, only, odd, even, etc.
  • Pseudo elementsbefore, after, placeholder, file, selection, marker, backdrop, first-letter, first-line, details-content.
  • Form statesdisabled, enabled, read-only, required, optional, valid, invalid, in-range, out-of-range, placeholder-shown, details-content, default, indeterminate.
  • Group/peer combinationsgroup-*, peer-* for every applicable parent selector.
  • has-, not-, in- — compound variants.
  • Media queries — breakpoints (sm, md, lg, xl, 2xl), dark, motion-safe, motion-reduce, print, screen, plus min-*/max-* arbitrary breakpoints.
  • Container queries@container, @<size>, @max-<size>.
  • Data and ARIAdata-[…], aria-[…].
  • *** and **** — direct/all descendants.
  • Logical statesrtl, ltr, forced-colors, inverted-colors, pointer-coarse, etc.

Every registration is right there in variants.ts; the file is large because it's exhaustive, not because individual registrations are complex.

@variant and @custom-variant

Users can register variants in CSS:

@custom-variant hocus (&:hover, &:focus);

@custom-variant supports-grid {
  @supports (display: grid) {
    @slot;
  }
}

Both forms hit the registration path in parseCss (in packages/tailwindcss/src/index.ts):

  • The selector form (@custom-variant name (sel1, sel2);) builds a static variant whose applyFn rewrites the rule's selector.
  • The body form (@custom-variant name { ... }) calls Variants.fromAst(...) which clones the body for each application and replaces @slot with the wrapped rule's children.

@variant (with no body) is the directive form used inside CSS:

.button {
  @variant hover {
    background: red;
  }
}

This is handled by substituteAtVariant in variants.ts, which calls applyVariant and re-emits the result.

Recent changelog entries (Unreleased):

  • Stacked @variant syntax (@variant hover:focus { … }) — PR #19996.
  • Compound @variant syntax (@variant hover, focus { … }) — PR #19996.

Plugin-registered variants

JS plugins call addVariant(name, selector) or matchVariant(name, fn). Both routes go through packages/tailwindcss/src/compat/plugin-api.ts, which translates them into Variants.static(...) / Variants.functional(...) calls.

Testing variants

packages/tailwindcss/src/variants.test.ts is ~2,200 lines of snapshot-based tests. Adding a new variant means:

  1. Register it in createVariants(theme) in variants.ts.
  2. Add a snapshot test in variants.test.ts.
  3. If the variant has interactions with compound, not, or group-*, add cross-cutting tests too.

For how the variant chain becomes wrapping rules see Utilities, and for how the candidate string is parsed in the first place see Scanning and extraction.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Variants – Tailwind CSS wiki | Factory