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:
static—hover:,focus:,dark:. TheapplyFntakes aRuleand rewrites it in place.functional—data-[state=open]:,aria-[checked]:. Takes a parameter; theapplyFnreadsvariant.valueto construct the wrapper.compound— wraps another variant. Implemented inapplyVariantrather 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
notvariant cannot negate sibling at-rules (would require transforming OR into AND), so when its inner produces multiple siblings the function returnsnulland the candidate is discarded. applyVariantreturnsnullwhen the variant is incompatible with the rule. The candidate then yields no CSS.- For compound variants, the implementation creates a placeholder
@slotAtRule, 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 classes —
hover,focus,active,visited,target,first,last,only,odd,even, etc. - Pseudo elements —
before,after,placeholder,file,selection,marker,backdrop,first-letter,first-line,details-content. - Form states —
disabled,enabled,read-only,required,optional,valid,invalid,in-range,out-of-range,placeholder-shown,details-content,default,indeterminate. - Group/peer combinations —
group-*,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, plusmin-*/max-*arbitrary breakpoints. - Container queries —
@container,@<size>,@max-<size>. - Data and ARIA —
data-[…],aria-[…]. - **
*and**** — direct/all descendants. - Logical states —
rtl,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 whoseapplyFnrewrites the rule's selector. - The body form (
@custom-variant name { ... }) callsVariants.fromAst(...)which clones the body for each application and replaces@slotwith 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
@variantsyntax (@variant hover:focus { … }) — PR #19996. - Compound
@variantsyntax (@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:
- Register it in
createVariants(theme)invariants.ts. - Add a snapshot test in
variants.test.ts. - If the variant has interactions with
compound,not, orgroup-*, 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.