Diagnosing Missing Tailwind Utilities When Classes Are Built Dynamically
A diagnostic guide for when Tailwind CSS utilities disappear in production because the JIT scanner misses dynamically constructed class names. Covers recognizable symptoms, a cause table, ordered checks, targeted fixes, and escalation steps.
14 Jul 2025, 00:56 UTC

The Problem: Classes Work in Dev, Vanish in Production
You run npm run dev and the UI looks correct. After npm run build the same component renders without the expected background, text color, or spacing. Inspecting the production CSS shows the utility class—say bg-red-500—is simply not there. The class string exists in your source, but Tailwind’s JIT scanner never saw it as a complete token.
Takeaway: Tailwind’s content scanner is regex‑based and only detects class names that appear as literal, complete strings in the files it scans. Any runtime construction—template literals, concatenation, array joins—breaks detection.
Root‑Cause Patterns
| Pattern | Detected by JIT? | Why It Fails |
|---|---|---|
text-red-500 (static string) | Yes | Full token present in source |
text-${color}-500 (template literal) | No | Variable value unknown at scan time |
["text-red-500","bg-blue-500"].join(" ") | No | Dynamic expression, no literal tokens |
cond ? "text-red-500" : "text-blue-500" | Yes | Both complete classes appear in source |
"text-" + color + "-500" | No | Concatenation yields no complete token |
Ordered Diagnostic Checks
- Search for dynamic construction. Run a repo‑wide grep for the patterns that evade the scanner:
Run this from the project root. It requires read access to source files. Matches indicate where you must rewrite or safelist.grep -R -E '\$\{|\+.*-|join\(|\?.*:.*-' --include="*.js" --include="*.tsx" --include="*.vue" . - Verify
contentglobs. Opentailwind.config.jsand confirm every template, component, and page file is covered. Add a temporary log to see resolved paths:
Run// tailwind.config.js console.log(JSON.stringify(config.content, null, 2));npx tailwindcss --help(any command) to print the array. Missing directories are a common cause. - Produce a minified build and grep for the missing class.
Run in the project root with Node ≥ 18. If the class is absent, the scanner missed it.NODE_ENV=production npx tailwindcss -i ./src/input.css -o ./dist/output.css --minify grep -n "bg-red-500" ./dist/output.css - Check for
@sourceinline directives or safelist usage. Look for@source "../components/**"in CSS files or asafelistarray in the config. These are intentional overrides; if they’re missing the class, the issue is detection, not purge.
Fixes Mapped to Findings
Template Literals → Enum Mapping
Replace text-${color}-500 with a lookup object that contains full class names:
const colorMap = {
primary: "text-blue-500",
danger: "text-red-500",
warning: "text-yellow-500"
};
// usage
colorMap[theme] // returns a complete token the scanner sees
This keeps the dynamic choice while giving the scanner literal strings.
Array Joins → Inline or Safelist
If you build class lists via join, either write the classes inline where possible or add the finite set to safelist in tailwind.config.js:
safelist: [
"text-red-500",
"bg-blue-500",
"p-4",
"rounded-lg"
]
Avoid broad regex safelists like { pattern: /bg-.*/ }—they bloat the output CSS with every matching utility.
String Concatenation → Complete Tokens
Rewrite "text-" + color + "-500" using the same enum approach or a helper that returns full class names. If the value truly comes from an external source (CMS, user input), safelist the known possibilities.
Missed Files → Expand content Globs
Add the missing directories to content. For monorepos, use absolute paths or workspace‑relative globs:
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"../packages/ui/**/*.{js,ts,jsx,tsx}"
]
Escalation Criteria
If the class still disappears after rewriting dynamic patterns and confirming content coverage, proceed through these steps:
- Confirm Tailwind version ≥ 3.3. Run
npx tailwindcss --version. Older JIT implementations had different scanning quirks. - Inspect PostCSS pipeline. Custom plugins (e.g.,
postcss-purgecss,cssnanowith aggressive rules) can strip utilities after Tailwind generates them. Temporarily disable non‑Tailwind plugins and rebuild. - Verify
@tailwind utilitiespresence. Open the generated CSS and ensure the directive block exists. If the framework’s integration (Next.js, Vite, Remix) transforms source before Tailwind scans, the scanner may see transpiled output instead of your original templates. Use the framework‑specific Tailwind plugin (e.g.,@tailwindcss/postcssfor Vite,next-plugin-tailwindcssfor Next.js) and pointcontentto pre‑transformed files. - Create a minimal reproduction. A single HTML file with a script tag that constructs the class dynamically, then run the Tailwind CLI directly. If the bug persists, file an issue with that repo.
Verification Checklist
- Production build (
NODE_ENV=production npx tailwindcss -i input.css -o output.css --minify) includes the previously missing class. - Grep for dynamic patterns returns zero matches after rewrites.
console.logof resolvedcontentglobs shows every entry point.- Temporary safelist addition makes the class appear—confirming detection, not purge, was the issue.
- Minimal reproduction with Tailwind CLI alone works, isolating framework integration problems.
Limitations & Practical Notes
- JIT scanning is regex‑based, not AST‑aware. It cannot evaluate imports, runtime variables, or complex expressions.
- Inline
@sourcedirectives (v3.4+) only work in files Tailwind already processes; they cannot pull in external dynamic strings. - Framework file transformations (Next.js SWC, Vite Vue SFC) may alter source before Tailwind scans. Ensure
contentpoints to pre‑transformed files or use the official framework plugin. - Safelist bloats output CSS; prefer static enumeration over broad patterns.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.