Missing styles in production almost always come from class names the scanner couldn't see.
Tailwind doesn't run your code. Its engine (Oxide, written in Rust) scans files as plain text, extracting every token that could be a class name, and generates CSS for those that match a utility. Unknown tokens are ignored, so scanning is fast and over-inclusive rather than precise.
Consequences:
- Class names must appear complete in the source.
text-${color}-600is never seen; map props to full strings instead:{ red: "text-red-600", green: "text-green-600" }[color]. - Classes in comments or strings still generate CSS (harmless).
- Classes that only arrive at runtime, such as from a database or CMS, need to be listed somewhere the scanner looks.
What gets scanned: everything in the project except files in .gitignore, binary files, CSS files and lock files. The base path is the current working directory. Tools for edge cases:
@source "../node_modules/@acme/ui";adds a path, such as a UI library in node_modules (ignored by default).@source not "./legacy";excludes a path.@import "tailwindcss" source("../src");sets the base path;source(none)disables automatic detection so only explicit@sourcepaths are used, which suits monorepos.@source inline("underline bg-red-{100..900..100}");safelists classes, with brace expansion for ranges.
The build runs incrementally: only changed files are rescanned and new utilities appended, which is why rebuilds take milliseconds. Output is then processed by Lightning CSS for vendor prefixes, nesting and minification.
Example
/* app.css */
@import "tailwindcss" source("../src"); /* scan from src/ only */
@source "../node_modules/@acme/ui/dist"; /* plus a component library */
@source not "../src/legacy"; /* but not this folder */
@source inline("bg-{red,amber,green}-{100,500}"); /* safelist CMS colours */
/* In components (JSX), never build names dynamically:
BAD : <span className={`bg-${status}-500`} /> never generated
GOOD: const color = { ok: "bg-green-500", warn: "bg-amber-500", error: "bg-red-500" };
<span className={color[status]} /> */Remember
- The scanner reads files as text: class names must appear complete
- node_modules and .gitignore'd paths are skipped by default
- @source adds or excludes paths; source() sets the base
- @source inline() safelists classes
Watch out for
- Constructing class names from string fragments and seeing styles only in development, when they happened to exist elsewhere.