One Compiler, Many Build Lifecycles

Tailwind CSS uses one core compiler across several environments, but it does not give every environment the same lifecycle. This chapter traces what persists and what changes in the CLI, Vite, PostCSS, bundler loaders, and browser build. The useful distinction is operational: a new class may need only an incremental candidate scan, while a changed stylesheet or plugin changes the compiler state itself and requires a full rebuild.

That distinction also clarifies ownership. The core compiler decides what a candidate means and how its CSS is ordered. The host adapter decides how CSS and source files are resolved, which files are watched, when the scanner runs, how dependencies are registered, and how output reaches the next tool. Treating those responsibilities as one system explains both Tailwind’s reuse and its host-specific surprises.

One compiler, different owners of time and state

Every build-oriented adapter begins with roughly the same handoff. It receives a CSS root, gives that CSS to the core compiler, reads the compiler’s source metadata, creates an Oxide scanner, collects candidates, and calls the compiler’s build function. The compiler owns the design system and generated CSS. The adapter owns the environment around it: file resolution, dependency messages, watch subscriptions, output writing, and optimization.

The CLI makes the sequence visible. On an initial build it reads the input stylesheet, creates the compiler, determines the automatic or explicit source roots, adds compiler-declared sources, and creates a scanner. The scanner walks the roots and returns candidates. The compiler builds the CSS, and the CLI writes it to a file or standard output. Optional optimization happens after the core CSS has been produced, so minification is a host-level output concern rather than a change to utility semantics.

Watch mode adds persistent state. The CLI remembers paths whose changes require a full rebuild, including the input CSS and dependencies discovered through imports, configuration, or plugins. It also uses scanner files and globs to learn which content changes should be considered. A file event can therefore be classified before the compiler runs: a change to a tracked stylesheet dependency invalidates compiler state, while a change to a content file can often be handled by scanning that file for new candidates.

Vite uses the same separation but fits it into the bundler’s module and environment model. A Vite root is associated with a CSS root and environment, and it keeps a compiler, scanner, accumulated candidates, and build-dependency information alive across transformations. The plugin registers discovered files and source globs with Vite, so the bundler can trigger the Tailwind root when relevant files change. Serve and full-build hooks share the same general lifecycle, while the host controls when optimization is appropriate.

PostCSS has a different interface because it is called as part of a CSS processing graph. Its adapter caches the compiler, scanner, Tailwind AST, converted PostCSS AST, optimized AST, input CSS, modification times, and paths that require full rebuilds. It reports imported files and scanner dependencies back to PostCSS through dependency messages. If the generated input CSS changes—even when no file’s modification time does, as can happen when another preprocessor supplies the input—the adapter treats that as a full rebuild condition.