Open-Source Wikis

/

Tailwind CSS

/

Tailwind CSS

/

Architecture

tailwindlabs/tailwindcss

Architecture

Tailwind CSS v4 has two cooperating engines:

  1. A CSS compiler written in TypeScript (packages/tailwindcss). It parses the input CSS, builds a design system from @theme and plugins, and turns a list of class names into a stylesheet.
  2. A content scanner written in Rust (crates/oxide). It walks the project, reads source files, and extracts every string that might be a Tailwind class.

A thin NAPI binding (crates/node) exposes the scanner to JavaScript as the @tailwindcss/oxide package, and a set of bundler adapters (@tailwindcss/cli, /vite, /postcss, /webpack, /browser) wire the two halves together for a particular host environment.

The build, end to end

graph LR
    Source[Source files<br/>HTML/JS/MDX/...]
    Input[Input CSS<br/>@import 'tailwindcss']
    Adapter["Adapter<br/>(Vite/PostCSS/Webpack/CLI)"]
    Scanner["@tailwindcss/oxide<br/>Scanner"]
    Compiler["tailwindcss.compile()"]
    Build["compiler.build(candidates)"]
    Optimize["@tailwindcss/node<br/>optimize() → lightningcss"]
    Output[Output CSS]

    Input --> Adapter
    Adapter -->|input CSS| Compiler
    Compiler -->|design system + sources| Build
    Source --> Scanner
    Adapter -->|sources| Scanner
    Scanner -->|candidates| Build
    Build -->|raw CSS| Optimize
    Optimize --> Output

The contract between adapter and compiler is two functions exported from packages/tailwindcss/src/index.ts:

  • compile(css, options) parses the input CSS, registers everything (@theme, @utility, @variant, @plugin, @config, @source, etc.), and returns an object exposing build(candidates), buildSourceMap(), features, root, and sources.
  • compileAst(ast, options) is the same pipeline but operates on an already-parsed AST. It is used by @tailwindcss/postcss so the PostCSS AST can be shared.

Adapters call compile once, ask Scanner for candidates whenever files change, then call compiler.build([...candidates]) to produce CSS.

The compiler pipeline

graph TD
    A[Input CSS string] -->|css-parser.ts| B[CSS AST]
    B -->|substituteAtImports| C[After @import resolution]
    C -->|walk: collect at-rules| D[Theme + variants + utilities + plugins + sources]
    D -->|applyCompatibilityHooks| E[v3 plugins/config bridged into design system]
    E -->|buildDesignSystem| F[DesignSystem]
    F -->|substituteAtApply, substituteAtVariant, substituteFunctions| G[Static AST<br/>without @tailwind utilities]
    G -->|build candidates → compileCandidates| H[Per-candidate rules]
    H -->|sort by variant + property order| I[Final AST]
    I -->|toCss + optimizeAst| J[Output CSS string]
  • CSS parsing lives in packages/tailwindcss/src/css-parser.ts. It produces the AST node types defined in packages/tailwindcss/src/ast.ts (StyleRule, AtRule, Declaration, Comment, Context, AtRoot).
  • @import resolution in packages/tailwindcss/src/at-import.ts calls back to a loadStylesheet function the adapter provides so imports can resolve through Vite/Webpack/enhanced-resolve.
  • The first walk through the AST (packages/tailwindcss/src/index.ts, parseCss) collects @theme, @layer, @utility, @custom-variant, @variant, @plugin, @config, @source, @apply, theme(...), and --spacing(…) usages. It also picks up the legacy v3 important, prefix, darkMode, and screen behavior through @theme.
  • Plugins and JS configs loaded via @plugin or @config are bridged through packages/tailwindcss/src/compat/ (see apply-compat-hooks.ts, plugin-api.ts, apply-config-to-theme.ts). The compat layer translates v3 addUtilities/matchUtilities/addVariant calls into v4 utility/variant registrations.
  • Candidate compilation is in packages/tailwindcss/src/compile.ts and packages/tailwindcss/src/utilities.ts. parseCandidate (in candidate.ts) parses a string like md:hover:bg-red-500/50 into a typed Candidate. compileCandidates then asks each utility's compileFn to emit AST nodes, applies any variants, and sorts by global property order (packages/tailwindcss/src/property-order.ts).
  • optimizeAst in packages/tailwindcss/src/ast.ts deduplicates rules, merges adjacent declarations, and applies the @property and color-mix polyfills.

The scanner pipeline

graph TD
    A[Sources: globs + base dirs] --> B[Walker<br/>ignore + globwalk]
    B --> C[Per-file fast-skip check<br/>fast_skip.rs]
    C -->|kept| D[Pre-processor<br/>per extension<br/>extractor/pre_processors/]
    D --> E[Extractor state machines<br/>candidate_machine.rs et al.]
    E --> F[Candidate strings]
    F --> G[Boundary trimming<br/>boundary.rs]
    G --> H[Set of unique candidates]
  • The walker comes from the vendored crates/ignore crate; it respects .gitignore, .tailwindcssignore, hidden files, and binary extensions defined in crates/oxide/src/scanner/auto_source_detection.rs.
  • The extractor is implemented as a stack of small state machines (crates/oxide/src/extractor/*_machine.rs) sharing a Cursor over the byte slice. Each machine recognizes one shape: candidates, named utilities, named variants, modifiers, arbitrary values, arbitrary properties, arbitrary variables, CSS variables, strings, and brackets.
  • Pre-processors normalize per-language quirks before extraction. They live in crates/oxide/src/extractor/pre_processors/ (Haml, Pug, Razor, Ruby, Slim, Svelte, Vue, etc.).
  • Scanner::scan and Scanner::scan_content are exposed via crates/node/src/lib.rs as the Scanner class in @tailwindcss/oxide.

How adapters compose the two halves

Every adapter follows the same shape:

  1. Build (or fetch a cached) compile(...) result for the input CSS.
  2. Construct (or update) a Scanner from compiler.sources, compiler.root, and the host's notion of the project base directory.
  3. On each rebuild: scan changed content, accumulate candidates, call compiler.build([...candidates]), optionally pass through optimize(...) from @tailwindcss/node.

The differences are mostly about plumbing: @tailwindcss/vite (packages/@tailwindcss-vite/src/index.ts) registers itself as multiple Vite plugins covering pre/post environments, transforms, watch invalidations, and HMR. @tailwindcss/postcss (packages/@tailwindcss-postcss/src/index.ts) caches per-input file in a QuickLRU and converts between PostCSS and the internal AST through cssAstToPostCssAst / postCssAstToCssAst (packages/@tailwindcss-postcss/src/ast.ts). @tailwindcss/webpack (packages/@tailwindcss-webpack/src/index.ts) adds dependency tracking via webpack's addDependency/addContextDependency. @tailwindcss/cli (packages/@tailwindcss-cli/src/commands/build/index.ts) drives the loop manually using @parcel/watcher.

The browser build (packages/@tailwindcss-browser/src/index.ts) replaces the Rust scanner entirely with a MutationObserver that walks document.querySelectorAll('[class]') and feeds class lists straight into compiler.build(...).

Performance design notes

  • Candidates are strings, not parsed objects. The scanner outputs raw strings; the compiler is responsible for parsing them via parseCandidate. A DesignSystem keeps an invalidCandidates set so duplicate failed parses are short-circuited.
  • Caches are invalidated by file mtime, not content hashes. Each adapter keeps an mtimes Map keyed by file path. @tailwindcss/postcss, @tailwindcss/webpack, and the CLI watch file mtimes via fs.statSync; touching a file forces a "full rebuild" that re-runs compile().
  • Features flags drive bail-out. Features (in packages/tailwindcss/src/index.ts) is a bit-set indicating which Tailwind features the input CSS actually uses. Adapters check it to skip work when a CSS file uses no Tailwind features.
  • The compiler returns a static AST that is reused. compile() returns a function build(candidates) that re-runs only the candidate compilation step on top of a cached parsed AST. This is why repeat builds in dev mode are fast.
  • Polyfills and minification run once at the end. @property fallbacks and color-mix(...) fallbacks are emitted by optimizeAst. Final minification goes through Lightning CSS in packages/@tailwindcss-node/src/optimize.ts.

Cross-cutting concerns

  • Source maps are produced by packages/tailwindcss/src/source-maps/ and remapped through the adapters via toSourceMap in packages/@tailwindcss-node/src/index.ts. See Source maps.
  • v3 compatibility is implemented in packages/tailwindcss/src/compat/ and bridges JavaScript configs and plugins. See v3 compatibility.
  • Variantshover:, md:, dark:, group/peer, has-, not-, etc. — are registered in packages/tailwindcss/src/variants.ts and applied during candidate compilation. See Variants.
  • The "design system" is the runtime context bundling the theme, registered utilities, variants, prefix, and important state. See Design system.

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

Architecture – Tailwind CSS wiki | Factory