Making Dynamic Tailwind Classes Work with JIT: A Practical Guide
Learn why Tailwind JIT misses dynamically generated class names and how to fix it with safelists or literal strings, plus a quick checklist to keep your CSS small and predictable.
28 Sept 2026, 21:12 UTC

The problem: dynamic class strings disappear in production
When you build a UI with Tailwind CSS v3+ and try to compose class names at runtime—for example, text-${color}-500 in a React component—you may notice that the expected utility is missing from the final CSS. The page looks fine in development because the JIT watcher sees the literal string during dev server reloads, but the production build omits it entirely. This leads to inconsistent styling and confusing debugging sessions.
Why it happens: JIT only sees what it can scan
The Just‑In‑Time compiler scans the files listed in the content array of your tailwind.config.js. It extracts class names that appear as plain strings in those files. If a class is assembled by string interpolation or concatenation, the scanner cannot predict the result, so the utility is never generated. The same limitation applies to any dynamic construction that is not literally present in the source files.
Worked example: fixing a dynamic text‑color utility
Assume a React project with the following setup:
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
theme: { extend: {} },
plugins: [],
};
// src/input.css
@tailwind base;
@tailwind components;
@tailwind utilities;
A component that tries to set a color based on a prop:
// src/Button.jsx
import React from 'react';
export default function Button({ variant, children }) {
// variant can be 'blue', 'gray', etc.
const colorClass = `text-${variant}-500`;
return (
{children}
);
}
If you run a development build with npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch, you’ll see the utility appear because the dev server recompiles on each change and the JIT sees the literal string during the refresh. However, a production build (NODE_ENV=production npx tailwindcss -o ./dist/output.css) will omit text-blue-500 and text-gray-500.
Solution 1: Safelist the pattern
Add a safelist entry that tells JIT to generate utilities for any text-*-500 class, even if it never sees the literal string:
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
safelist: [
{ pattern: /text-(blue|gray)-500/, variants: ['hover', 'focus'] }
],
theme: { extend: {} },
plugins: [],
};
Now run the production build again and inspect the output:
npx tailwindcss -o ./dist/output.css
# then check
grep -E 'text-(blue|gray)-500' ./dist/output.css
You should see the corresponding CSS rules.
Solution 2: Write the class fully
If the set of possible values is small and static, avoid interpolation altogether:
// src/Button.jsx
import { cn } from 'clsx'; // or classnames
export default function Button({ variant, children }) {
return (
{children}
);
}
Because each possible class appears as a literal string in the scanned file, JIT will generate them without any safelist.
Trade‑off: flexibility vs. design‑token discipline
Using safelists or arbitrary values (w-[327px], bg-[#1a2b3c]) gives you on‑the‑fly styling power, but it can erode the consistency Tailwind aims to provide. A pragmatic approach is to:
- Allow arbitrary values only for one‑off layout dimensions (e.g., custom widths, heights).
- Keep colors, spacing, and typography strictly tied to the design token system defined in
tailwind.config.js. - Document the rule in a contributor guide and enforce it with a lint rule that flags arbitrary color or spacing values.
This keeps the majority of your styling predictable while still permitting occasional escapes when a design truly needs a unique value.
Actionable checklist
- Verify your
contentglobs cover every template file (including those generated by frameworks). Runnpx tailwindcss -vvto see which files are scanned. - If you use dynamic class strings, decide whether to safelist the pattern or rewrite them as literal classes.
- After changing the config, run a production build and check the output with
greporrgfor the expected utilities. - Measure the generated CSS size:
gzip-size dist/output.css. It should stay in the low‑single‑digit kilobyte range for a typical component set. - Add a lint rule (e.g.,
eslint-plugin-tailwindcss) to prohibit arbitrary color or spacing values unless explicitly allowed.
Following these steps ensures that your dynamic Tailwind utilities survive the JIT compilation step and that your styling system remains both flexible and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.