Why Your Dynamic Tailwind Classes Disappear and How to Fix Them
Tailwind only generates CSS for class names it finds as complete literal strings. Dynamic construction like `bg-${color}-500` fails silently. Fix it with a typed map module for values you control, or a narrow allowlist for external input—then verify by searching the emitted stylesheet.
09 Sept 2026, 13:14 UTC

The Silent Failure
You write bg-${theme}-500 expecting a blue background. The component renders. No error, no warning—just a missing style. This is Tailwind's most common "works on my machine" trap: the scanner only sees complete literal strings in your source files. It does not execute JavaScript, evaluate template literals, or concatenate strings at build time.
How Detection Actually Works
Tailwind's engine scans your configured source paths (the content globs in older versions, or automatic detection plus explicit paths in newer ones) and extracts every token that looks like a utility class. The extraction is a regex pass over static text. If the class bg-blue-500 appears nowhere as a contiguous string, the corresponding CSS rule is never emitted. Constructed strings like `text-${size}` or 'p-' + scale leave zero trace for the scanner.
The Mapping Pattern: Keep Classes Greppable
The most portable fix is to map runtime values to whole class strings in a single module that the scanner can see.
// themeMap.ts — scanned because it's imported
export const bg = {
primary: 'bg-blue-600',
danger: 'bg-red-600',
neutral: 'bg-gray-200',
} as const;
export const text = {
sm: 'text-sm',
base: 'text-base',
lg: 'text-lg',
} as const;
Usage becomes className={bg[theme]}. Every candidate class exists verbatim in a file Tailwind scans, so the CSS is generated. The map is also type-checkable and greppable—grep -r "bg-blue-600" shows exactly where the utility originates.
When Mapping Isn't Enough: Explicit Allowlists
Classes that come from a CMS, a database, or user input cannot live in your source tree. For those, add an allowlist (sometimes called a safelist) in your Tailwind configuration. The exact key name varies by major version—check the docs for your installed version—but the concept is the same: a list of class names or patterns that are forced into the output regardless of scanner results.
// Example shape; verify the exact key for your version
module.exports = {
safelist: [
'bg-emerald-500',
'bg-amber-500',
{ pattern: /^bg-.*-500$/ },
],
};
Use patterns sparingly. A broad regex like /^bg-.*-500$/ pulls in every background shade at the 500 stop, inflating the stylesheet and hiding dead code.
Worked Example: Literal vs. Constructed
Create a minimal reproduction to prove the behavior.
- Add two elements in a template:
<div class="bg-blue-500">literal</div> <div class={ `bg-${color}-500` }>constructed</div> - Build the project (
npm run buildor equivalent). - Open the emitted CSS and search for
.bg-blue-500.- The literal class selector will be present.
- The constructed variant will be absent unless
coloris a literal'blue'somewhere in a scanned file.
This two-element test isolates the scanner's behavior without any extra tooling.
Trade-off: Bundle Size vs. Completeness
Every class in an allowlist or matched by a pattern adds CSS rules to the output. Widening content globs to catch more files has the same effect. The trade-off is explicit: you accept a larger stylesheet to guarantee that dynamic or external classes render. Measure the impact by comparing the gzipped CSS size before and after adding the allowlist. If the delta is small, the safety is worth it; if it grows substantially, refine the pattern or move more classes into a mapped module.
A Note on Conflicting Utilities
When two utilities set the same CSS property (e.g., p-2 and p-4), the winner is determined by their order in the generated stylesheet, not by their order in the class attribute. This is why class-merging libraries exist—they deduplicate at the utility level before the class string reaches the DOM. If you adopt one, treat it as a separate dependency decision, not a Tailwind configuration change.
Actionable Closing Checklist
- Confirm your Tailwind major version from
package-lock.jsonorpnpm-lock.yamland read the matching documentation for content detection and allowlisting. - Run a production build and search the output CSS for a class you expect to be generated.
- If it's missing, add the two-element reproduction above to verify the scanner is the cause.
- For classes you control, refactor to a typed map module so every utility appears as a literal string.
- For truly external classes, add a narrow allowlist entry and re-measure the CSS size.
Predictable styling in Tailwind comes from respecting the scanner's static nature. Make the classes visible, and the framework does the rest.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.