Web Development

Tailwind JIT classes stripped in Next.js production build when using dynamic template literals for class names

Environment

Environment

Next.js 13 app using Tailwind CSS JIT (tailwindcss v3) with app/router, production build on Vercel; codebase uses dynamic className construction via template literals and concatenation (e.g., className={${base} ${cond ? text-${color}-500 : ''}}) and some class names are assembled from variables.

Symptoms

Symptoms

Certain UI elements lose styling on production build (styles present in local dev server). Specific classes like text-red-500 or md:grid-cols-4 are missing from final CSS, causing layout/styling regressions only after next build --production; clearing cache and redeploy doesn't fix until using explicit class names in JSX.

Observations

Observations

Tailwind output CSS size is smaller in production; scanning generated CSS shows missing utility selectors that should have been generated; running 'npx tailwindcss -i ./input.css -o ./out.css --content' locally with same content glob produces correct CSS when variable class strings are replaced with literal strings; project's tailwind.config.js content array includes './app//*.{js,jsx,ts,tsx}', './components//*.{js,ts,tsx}', and a custom extractor plugin; PostCSS pipeline is standard; Next.js SWC minifier enabled.

Already attempted

Already attempted

Added safelist entries for missing classes in tailwind.config.js (temporarily restored styling); replaced some dynamic template literal cases with classnames library and explicit strings (fixes those components); disabled SWC minify (no change); examined Vercel build logs for Purge warnings (none); ran tailwind JIT CLI locally replicating production build and reproduced missing classes when using dynamic assembly.

Question

Question

How can Tailwind JIT miss classes constructed via nested template literals at build time in Next.js production builds, and what safe, reversible steps should I follow to make dynamic class names reliable without bloating CSS?

COMMUNITY RESPONSES

Verified community troubleshooting

0 replies

No answers yet..

Showing 0 of 0 community responses
AI DIAGNOSTICS

Incident analysis

Updated 2d ago• Trouble ID

Most likely causes

Dynamic class names built from runtime variables or nested template literals that tailwind's static scanner cannot detect in production 95%
Use of computed names using variables derived from props or external data that the static analyzer cannot resolve 90%
Custom extractor or content config that omits some file paths from Tailwind's content scan 30%

Summary

Tailwind JIT scans source files for literal class name strings. When class names are assembled from variables or nested template literals, JIT cannot statically detect them unless safelisted or generated via deterministic mapping at build time. The observed behavior (works in dev but not prod) often occurs because the dev JIT runs at runtime with full file access and dynamic evaluation, while the production Purge/content scanning is static and misses dynamic patterns. Safelist fixed it, indicating missed detection rather than a tool bug.

Suggested Fixes

Diagnostic step 1
Identify patterns: search repository for template literal patterns and computed class strings (e.g., regex for `text-\${` or `\${color}-500`) and list all dynamic cases to decide per-case fix.
Diagnostic step 2
Use safelist for true dynamic values: in tailwind.config.js add a safelist with a compact range (e.g., { pattern: /^text-(red|green|blue)-(100|200|300|400|500)$/ }) rather than enumerating every possible class to keep generated CSS minimal and reversible.
Diagnostic step 3
Replace dynamic assembly with deterministic mapping: convert constructs like `text-${color}-500` into a small lookup object mapping color names to class strings (e.g., const colorClass = { red: 'text-red-500' }[color]) so Tailwind sees literal class names.