From Stylesheet to Generated Rules
This chapter follows Tailwind CSS’s core compiler from a stylesheet to generated rules. The focus is not the syntax of one directive or the internals of one package, but the state transitions that decide what the compiler can generate, how it orders the result, and why a later class can often be added without rebuilding everything. With that control flow in view, a missing rule becomes a stage-specific problem rather than a mysterious failure.
The key distinction is between creating a compiler and asking that compiler to build a candidate set. Tailwind first reads CSS as a description of the design system and its boundaries. It then receives strings discovered in project sources, resolves them against the state established by that CSS, and prints an ordered stylesheet. Those activities are related, but they do not have the same lifetime.
The stylesheet installs the compiler’s state
Tailwind begins by parsing the input CSS into an abstract syntax tree, or AST. An AST is a structured representation of rules, declarations, and at-rules; it lets the compiler inspect and transform the stylesheet without treating it as an opaque string. Imports are expanded early, so the compiler can reason about the theme, base styles, utilities, and project extensions as one input rather than as disconnected files.
The parser then walks the resulting structure and collects the directives that affect later work. A utilities marker tells Tailwind where generated utility rules will eventually be placed. A theme block contributes custom properties to the theme store. A source rule records a path or pattern that should contribute candidates, while inline source content can add explicit candidates immediately. Custom utility and variant rules are held until the design system is ready to register them.
The parser also normalizes context that would otherwise be easy to lose between files. A reference import can make theme and variant information available to a component stylesheet without emitting a second copy of the referenced styles. A source root can be attached to the utilities entry point, and a prefix or important setting can be carried into the generated rules. These choices are recorded while the stylesheet is still being interpreted, so later candidate compilation does not have to guess which surrounding context the class came from.
This collection step is more consequential than it may look. Consider three small changes to an input stylesheet. Adding a theme variable changes which values a functional utility can resolve. Adding a custom utility changes which candidate roots are valid. Adding a custom variant changes how an otherwise valid utility is wrapped and where it sits relative to other variants. These are not declarations that the browser discovers later; they are instructions that shape the compiler before it sees the application’s class strings.
Tailwind also records feature flags while it parses. The flags answer questions such as whether utilities, imports, variants, theme blocks, compatibility behavior, or @apply are present. This gives the surrounding host a way to avoid treating every CSS file as a Tailwind root. If the input contains no Tailwind feature that requires compilation, the system can preserve the input instead of constructing an unnecessary utility build.
The design system is assembled after the initial walk. It contains the theme, utility registry, variant registry, and caches that connect raw candidate strings to parsed candidates and generated AST nodes. Compatibility hooks can then merge legacy configuration or plugin behavior where the v4 surface still supports it. The separation matters: compatibility is applied at an explicit boundary rather than being mixed into every core utility definition.
The ordering of registration is also deliberate. Custom variants are collected first and then registered according to their dependencies, so a variant that refers to another custom variant can be resolved in a meaningful order. Custom utilities are registered into the same utility system used by built-ins. The result is a compiler state that already knows the vocabulary and transformation rules before any source file asks for output.
This is the first diagnostic checkpoint. If a stylesheet has a malformed directive, an invalid custom utility name, or a source path that cannot be accepted, the failure belongs to compiler setup. No amount of changing class strings in a template will repair it. Conversely, if the stylesheet parses and establishes the expected state, the next question is what happens to each candidate.
A candidate becomes an ordered rule
When the host supplies candidate strings, Tailwind does not concatenate those strings into CSS. It parses each one into a structured candidate. A plain utility such as underline has a different shape from a functional utility such as w-1/2, and both differ from an arbitrary property such as [mask-type:luminance]. Variants and modifiers are represented separately so the compiler can validate them and apply them in the correct phase.
Take a candidate such as dark:hover:bg-sky-700. The parser must identify the base utility, the value, and the two variants. The design system then asks whether bg is a known color utility, whether sky-700 resolves to a theme value, and whether the variants can be applied to the generated rule. The raw string is only the request. The utility registry and theme determine whether that request has a meaningful result.
For a named value, resolution usually follows a theme namespace. For an arbitrary value, the parser carries the value and any data-type hint forward so the utility can determine whether it is a color, length, percentage, or another supported kind. A modifier may alter a color’s opacity or participate in a fraction. If a candidate includes a modifier that a utility would silently ignore, the compiler can reject it instead of generating the same CSS as the unmodified candidate. That prevents a plausible-looking typo from appearing to work while doing nothing.
This separation between parsing and resolution is useful when one prefix can mean more than one thing. A text-related candidate might refer to a font size or a color, depending on the value. A value such as 22px gives the utility a length-shaped signal, while a color value gives it a different signal; a CSS variable may require an explicit type hint. The compiler can therefore preserve expressive syntax without treating every string as valid for every property. It first records what the candidate claims to be, then asks the relevant utility whether that claim is supported by the design system.
Once the base utility resolves, the compiler creates AST nodes for its declarations and applies each variant. A simple state variant can wrap a rule in a pseudo-class. A responsive or container variant can place it inside a conditional at-rule. A compound variant such as a group or peer relationship needs more care: it must modify the selector or surrounding rule that the nested variant produced, without accidentally applying the parent relationship to declarations that do not belong to it. The compiler uses a temporary slot for that composition step and replaces the slot with the actual utility nodes after the relationship has been applied.
Only after these transformations can Tailwind sort the rules. Variant order is considered first because conditions and state relationships determine how rules are grouped. Property order then gives the generated declarations a predictable position, with property count and candidate comparison resolving remaining ties. The purpose is not to mimic the order in which class names happened to appear in a template. It is to produce a stylesheet whose cascade follows the framework’s declared ordering model.
The sequence shows why the stylesheet is not rebuilt from scratch for every new class. The compiler keeps the parsed design system and the candidate-related caches alive. A later build can add newly discovered candidates to the known set, compile the resulting utility nodes, and update the utilities position in the AST. If no new candidate or used external variable changes, the compiler can return the already optimized result.
The reusable unit is larger than a single class but smaller than an entire application build. The compiler can cache the parse of a candidate, the parse of a variant, and the AST produced by a particular candidate under the current design system. That means repeated observations of the same class do not require identical work, while a genuinely new candidate still joins the existing set. The output remains derived from the complete known set, so sorting can be recomputed consistently rather than appending rules in arrival order.
That reuse has an important boundary. The cached build is designed around adding candidates; it does not assume that already known candidates are being removed during an incremental call. If the stylesheet itself changes, or if a theme, imported stylesheet, configuration file, or plugin changes the meaning of the design system, the host must arrange a stronger rebuild. Reusing a stale design system in that situation would be faster but wrong.
The same stateful design explains why invalid candidates are remembered. A string that failed to parse or resolve does not need to be reconsidered on every scan unless the compiler state is recreated. This is a correctness-preserving optimization: unsupported input is omitted, not converted into a guessed rule. It also gives the host a clean distinction between “the scanner found a token” and “the compiler accepted it.”
Finding the failing stage
The pipeline supports a practical debugging sequence. First ask whether the stylesheet compiled. If it did not, inspect imports and directives rather than the template. A malformed @theme, an invalid source declaration, or an incompatible extension stops before candidate generation.
If setup succeeded, ask whether the candidate reached the compiler. That is a source-detection question, owned by the scanner and the host’s source configuration. A class assembled from fragments may never appear as a complete candidate. A file under an ignored directory may not be scanned. The compiler cannot reject a candidate it never receives.
If the candidate did arrive, ask whether the base utility and its value resolved. flex is a static utility; a token-driven color or spacing class needs an appropriate theme value. An arbitrary property must contain a valid property and value. A named candidate can therefore be visible in source and still produce no rule because it has no valid utility match. This distinction is especially useful when a project has customized or narrowed its theme: the class can be spelled correctly and still be outside the vocabulary that the stylesheet installed.
Next ask whether a variant could be applied. An invalid compound relationship or unsupported combination can cause the candidate to be discarded after the base utility was valid. This is why a base class may work while a more elaborate variant chain does not. The failure is not necessarily in the theme or scanner; it may be in the transformation that turns a utility into a contextual rule.
Finally, separate compiler output from host delivery. The core can produce the expected CSS while a watcher, dependency registration, or bundler cache fails to trigger the build that would include it. That last boundary belongs to the integration lifecycle rather than to candidate compilation, and it becomes especially important in development mode.
Tailwind’s compiler is therefore less like a lookup table than a staged contract. CSS installs the available vocabulary and transformations. Candidate strings request particular outputs. The design system resolves them, variants transform them, and sorting gives the result a stable cascade position. The next question follows directly: where does that vocabulary come from, and how can a CSS variable determine which utility or variant names a project is allowed to use?