Why Tailwind CSS Dynamic Classes Break in       Production (And How to Fix It)

Meta description: I spent hours debugging missing Tailwind styles in production. Here’s exactly why dynamic classes get purged and the right way to fix it without bloating your CSS.

Last updated: May 2026


The Production Bug That Wasted My Afternoon

I had just deployed what I thought was a clean feature — a dynamic alert component that changed color based on severity. It looked perfect in development. Green for success, yellow for warning, red for error. Then I opened the production URL and everything was the same dull gray.

No console errors. No build failures. The classes were right there in the JSX. But the styles simply weren’t showing up.

After two hours of confusion, I finally understood the root cause — and it was entirely my fault. I had fallen into one of the most common Tailwind CSS traps: dynamically constructing class names at runtime.

If you’ve hit this wall, this guide will save you a lot of time.


TL;DR

  • Tailwind’s content scanner works at build time, not runtime — it cannot detect dynamically assembled class strings
  • The fix is to use complete class names in your source code, never string-concatenated partials
  • For truly dynamic styling, use a safelist in tailwind.config.js or switch to CSS variables

Why This Happens: Tailwind’s Build-Time Purging

Tailwind CSS uses a process called tree-shaking (or purging) to remove unused styles from your final CSS bundle. This is what makes Tailwind so fast in production — instead of shipping the full utility library, it ships only the classes that appear in your source files.

The scanner works by crawling the files you define in the content array of your tailwind.config.js and looking for class name strings. It’s essentially a regex-based text scan. It does not execute your JavaScript.

This is where developers get burned. If Tailwind’s scanner never sees the string "bg-red-500" written out completely, it won’t include that style in the output CSS.

Important: Tailwind’s documentation states explicitly: “The most important implication of how Tailwind scans your source files is that it will only detect class names that exist as complete, unbroken strings in your source files.” [SOURCE: https://tailwindcss.com/docs/content-configuration#dynamic-class-names]


Prerequisites

Before diving into the fixes, make sure you’re working with:

  • Tailwind CSS v3.x or v4.x
  • A build tool like Vite, Webpack, or Next.js
  • Basic familiarity with your tailwind.config.js file
  • Node.js 18+ (I ran into subtle issues with older versions and the JIT engine)

The Root Cause in Plain Code

Here’s the exact pattern that breaks production builds.

The Broken Pattern: String Concatenation

jsx

// ❌ This will NOT work in production
const alertColor = "red";
return <div className={`bg-${alertColor}-500 text-${alertColor}-800`}>Alert</div>;

At build time, Tailwind’s scanner sees bg-${alertColor}-500 — a template literal with a variable. It has no way to know what alertColor will be at runtime. So neither bg-red-500 nor any other color variant gets included in the CSS bundle.

In development, this often appears to work because many setups don’t purge CSS during dev mode. The moment you run npm run build and deploy, the styles vanish.


How to Fix It: Four Proven Approaches

Step 1 — Use Complete Class Name Strings (The Right Default)

The simplest and most reliable fix is to write full class names in your source code and let JavaScript select between them:

jsx

// ✅ This works perfectly
const colorMap = {
  success: "bg-green-500 text-green-800",
  warning: "bg-yellow-500 text-yellow-800",
  error: "bg-red-500 text-red-800",
};

return <div className={colorMap[severity]}>Alert</div>;

The scanner sees all three complete strings and includes all of them in the build. At runtime, JavaScript simply picks the right one. This is my go-to solution for most use cases.

Step 2 — Add a Safelist in tailwind.config.js

When you’re pulling class names from a database, a CMS, or an API — places where you genuinely cannot pre-define every value — use the safelist option:

js

// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  safelist: [
    "bg-red-500",
    "bg-green-500",
    "bg-yellow-500",
    {
      pattern: /bg-(red|green|yellow|blue)-(100|500|900)/,
    },
  ],
  theme: {},
  plugins: [],
};

The pattern option accepts a RegExp, which is powerful but should be used carefully. A pattern that’s too broad will bloat your CSS significantly. In my experience, matching entire color + shade combinations works well without causing bundle size problems.

Step 3 — Use clsx or cva for Variant Management

If you’re building a component library, I strongly recommend pairing Tailwind with clsx and class-variance-authority (cva). These libraries enforce the “complete strings” pattern by design:

bash

npm install clsx class-variance-authority

tsx

import { cva } from "class-variance-authority";

const alert = cva("rounded-md px-4 py-3 text-sm font-medium", {
  variants: {
    intent: {
      success: "bg-green-100 text-green-800",
      warning: "bg-yellow-100 text-yellow-800",
      error: "bg-red-100 text-red-800",
    },
  },
  defaultVariants: {
    intent: "success",
  },
});

// Usage
<div className={alert({ intent: "error" })}>Something went wrong</div>

Every variant is a hardcoded string — Tailwind’s scanner picks them all up cleanly. This is my preferred pattern for any component that lives in a shared UI library.

Step 4 — Fall Back to Inline Styles for Truly Dynamic Values

Sometimes the values themselves are dynamic — think user-chosen brand colors from a color picker stored in a database. In those cases, Tailwind isn’t the right tool for that specific style property. Use CSS custom properties instead:

jsx

// ✅ Use inline styles for values you can't know at build time
<div
  style={{ "--brand-color": user.brandColor } as React.CSSProperties}
  className="bg-[var(--brand-color)]"
>
  Custom branded section
</div>

This keeps Tailwind in control of layout and spacing while offloading truly unknown values to the CSS variable system.


Real-World Tips I Use in Production

Always test your production build locally before deploying. Run npm run build && npx serve dist (or the equivalent for your framework) and visually check dynamic components. This catches purging issues before they hit your users.

Keep your content array tight. I’ve seen projects where someone added "**/*.html" too broadly and accidentally included node_modules paths, which caused unpredictable scanning behavior. Be specific:

js

content: [
  "./index.html",
  "./src/**/*.{js,ts,jsx,tsx}",
],

Audit your bundle size if you use broad safelist patterns. Run npx tailwindcss --input ./src/index.css --output ./dist/audit.css and check the file size. A healthy production Tailwind file should rarely exceed 20–30kb before gzip.

Pro Tip: Install the Tailwind CSS IntelliSense extension for VS Code. It warns you in real time when it detects dynamically constructed class names that won’t be scanned correctly. [SOURCE: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss]


Common Errors and How I Fixed Them

Error: Styles present in dev, missing in production Cause: JIT mode is disabled or purging is active only in production. Fix: Confirm your content paths match the actual file structure. Use console.log to verify the class string being passed to className is complete and unbroken.

Error: Safelist pattern including too many classes Cause: An overly broad regex like /bg-.*/ matches every background color and shade, shipping thousands of unused utilities. Fix: Narrow the pattern. If you only use 500 and 100 shades, write /bg-(slate|red|green)-(100|500)/ explicitly.

Error: content array not picking up .tsx files Cause: Missing the tsx extension in the glob pattern. I hit this when migrating a project from JS to TypeScript mid-development. Fix: Update to "./src/**/*.{js,ts,jsx,tsx}" — all four extensions.


FAQ

Q: Why do my Tailwind classes work in development but not after I deploy to Vercel or Netlify? A: Development mode typically doesn’t purge CSS, so all Tailwind utilities are available. Production builds scan your source files and remove anything that doesn’t appear as a complete, static string. Dynamically constructed class names are invisible to this scan.

Q: Is it safe to add all Tailwind color utilities to the safelist? A: Technically yes, but it defeats Tailwind’s main performance benefit. You’d be shipping the entire color palette, which can add hundreds of kilobytes to your CSS. Use targeted safelists or the complete-strings approach instead.

Q: Can I use Tailwind with class names stored in a database or CMS? A: Yes, but you need to either safelist those classes explicitly in tailwind.config.js or store full utility strings (not partials) in the database and trust your content entry process to use valid Tailwind names.

Q: Does Tailwind v4 fix the dynamic class problem? A: Tailwind v4 introduces a new CSS-first configuration model and improved scanning, but the fundamental constraint remains — the scanner is still static at build time. Dynamic class construction is still a pattern to avoid.

Q: What is the best library to manage Tailwind variants in a component library? A: In my experience, class-variance-authority (cva) is the cleanest solution. It forces you to define all variants as static strings upfront, which is exactly what Tailwind’s scanner needs.


Conclusion

Dynamic Tailwind classes breaking in production is one of those bugs that feels mysterious until you understand the build-time scanning model — and then it makes complete sense. The solution isn’t complex: write complete class strings, use lookup maps, and reach for the safelist only when you genuinely can’t predict the values at build time.

Once I internalized this mental model, I stopped fighting Tailwind and started working with it. Your production styles will thank you.


About the Author: I’m a frontend engineer with over 8 years of experience building production web applications. My current stack centers around React, TypeScript, and Tailwind CSS, and I’ve shipped Tailwind-powered design systems used by teams across multiple time zones. I write about the practical, hard-won lessons that documentation doesn’t always cover.