Open-Source Wikis

/

Tailwind CSS

/

How to contribute

/

Debugging

tailwindlabs/tailwindcss

Debugging

DEBUG instrumentation

The Node packages all share an instrumentation helper at packages/@tailwindcss-node/src/instrumentation.ts. Every adapter respects a DEBUG env variable (packages/@tailwindcss-node/src/env.ts) that turns on timing output.

DEBUG=* pnpm --filter @tailwindcss/cli dev -- -i input.css -o out.css

You will see lines like:

[@tailwindcss/cli] (initial build) … 14.7ms
  Setup compiler … 5.2ms
  Scan for candidates … 3.4ms
  Build utilities … 4.1ms
  Optimization … 1.2ms

This is the same data path @tailwindcss/postcss, @tailwindcss/vite, @tailwindcss/webpack, the CLI, and the browser build all use. Each adapter calls using I = new Instrumentation() and wraps its work in I.start(name) / I.end(name) blocks. Search for Instrumentation in any adapter's index.ts to see what spans are reported.

The browser build's instrumentation (packages/@tailwindcss-browser/src/instrumentation.ts) reports to the browser performance timeline (performance.mark / performance.measure) instead of stderr.

Tracing the Rust scanner

The scanner uses the tracing crate. Enable it via the RUST_LOG env variable:

RUST_LOG=tailwindcss_oxide=debug cargo test -p tailwindcss-oxide -- --nocapture

Set up code lives in crates/oxide/src/scanner/init_tracing.rs. The first Scanner::new(...) call initializes the subscriber if RUST_LOG is set.

Common failure modes

"Scanner returned no candidates"

Usually means the scanner did not find your source files at all. Check:

  1. Is your file extension on the allow list in crates/oxide/src/scanner/auto_source_detection.rs?
  2. Are you adding it via @source in CSS or via the host's sources (Vite picks them up from transform/load hooks; PostCSS picks them up from result.opts.from).
  3. Is the file in .gitignore or .tailwindcssignore? The walker uses crates/ignore/, which respects both.
  4. For programmatic use: are you passing the right base to the Scanner?

"@apply doesn't pick up my custom utility"

The order of operations in parseCss (packages/tailwindcss/src/index.ts) matters: @utility registrations happen before @apply substitution. But if your custom utility lives in a JS plugin loaded by @plugin, the v3 compat hooks run after some passes. See packages/tailwindcss/src/compat/apply-compat-hooks.ts.

"My theme value isn't applied"

@theme blocks must be at the top level of the CSS file (no nesting). Inside @reference imports, @theme is treated as reference-only.

For programmatic checking: theme.get('--color-red-500') from Theme (packages/tailwindcss/src/theme.ts) tells you what value the design system actually resolved.

"Vite plugin keeps doing full rebuilds"

Vite's plugin records mtimes for every imported CSS or JS dependency. If a config file or plugin gets touched, the next change triggers a full rebuild (which clears the require-cache via clearRequireCache from @tailwindcss/node/require-cache). To debug, set DEBUG=*. The Register full rebuild paths span will show which files invalidated.

"PostCSS plugin: 'No such file' errors during HMR"

The PostCSS plugin caches per-(inputFile, base, optimize) triple. If your build system mutates base between runs, you'll get cache thrashing. Inspect the cache QuickLRU in packages/@tailwindcss-postcss/src/index.ts.

Running a specific candidate through the compiler

To debug a specific class compilation, write a unit test:

import { compileCss } from './test-utils/run'; // helper used throughout the suite

let result = await compileCss(`@import 'tailwindcss';`, [
  'md:hover:bg-red-500',
]);
console.log(result);

packages/tailwindcss/src/test-utils/ has small helpers that stand up a compiler instance with the standard imports. Most existing tests use them — pick one that's close and copy.

Inspecting the AST

packages/tailwindcss/src/ast.ts exports toCss(ast) and cssContext(node). To see the AST at any point:

import { walk } from './walk';

walk(ast, (node) => {
  console.log(node.kind, 'name' in node ? node.name : '');
});

The CSS parser also exports the typed AST node constructors (atRule, styleRule, decl, comment, context) so you can synthesize fragments by hand for tests.

When PostCSS gets in the way

@tailwindcss/postcss converts between the internal AST and PostCSS's AST in packages/@tailwindcss-postcss/src/ast.ts (postCssAstToCssAst / cssAstToPostCssAst). If a regression only reproduces under PostCSS, instrument the boundary: log the AST before and after the conversion. Source map information is preserved through the round-trip, so any divergence will show up in the dump.

When the upgrade tool refuses to run

@tailwindcss/upgrade requires a clean git worktree by default (see isRepoDirty() in packages/@tailwindcss-upgrade/src/utils/git.ts). Use --force to run anyway:

npx @tailwindcss/upgrade --force

It also re-checks installedTailwindVersion vs expectedTailwindVersion from packages/@tailwindcss-upgrade/src/utils/version.ts. If you hit a version mismatch, run pnpm install (or your equivalent) and try again.

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

Debugging – Tailwind CSS wiki | Factory