DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Why Your Tailwind Classes Work in Development and Silently Vanish in Production

Almost every Tailwind developer hits this exact bug once, usually right after a deploy, and the confusing part is that nothing about it looks wrong in the code, nothing throws an error, and it worked completely fine five minutes earlier on the same machine.

The Code That Looks Completely Reasonable

function StatusBadge({ status }: { status: 'success' | 'warning' | 'error' }) {
  const color = status === 'success' ? 'green' : status === 'warning' ? 'yellow' : 'red';

  return (
    <span className={`bg-${color}-100 text-${color}-800 px-2 py-1 rounded`}>
      {status}
    </span>
  );
}
Enter fullscreen mode Exit fullscreen mode

In development, this renders exactly as expected, colored badges for each status, built dynamically from a single reusable component instead of three separate hardcoded ones. Then you deploy, and every badge renders as a plain, unstyled span. No color, no padding in some setups, nothing, depending on exactly what else got purged alongside it.

Why This Happens: Tailwind Doesn't Actually Execute Your Code

Tailwind doesn't run your JavaScript to figure out which classes you're using. It scans your source files as plain text, looking for strings that match known utility class patterns, and only generates CSS for the classes it actually finds written out, literally, somewhere in that scanned text.

className={`bg-${color}-100 text-${color}-800`}
Enter fullscreen mode Exit fullscreen mode

Tailwind's scanner sees the literal text bg-${color}-100, not bg-green-100, bg-yellow-100, or bg-red-100. Template literal interpolation happens at runtime, in the browser or on your server, long after Tailwind's build step has already finished scanning your source files and generating the final CSS. Since none of the three actual color classes your component needs ever appears as literal, complete text anywhere in your codebase, Tailwind never generates CSS for any of them, and the class names that do get applied to the DOM at runtime simply don't exist in your stylesheet at all.

Why This Specifically Survives Local Development

This is the part that makes the bug genuinely sneaky rather than just a misunderstanding: in development mode, especially with certain dev server configurations or cached builds from before the dynamic class was introduced, you can sometimes see the correct styling anyway, either because an earlier full build still has the class generated from some other literal usage elsewhere in the project, or because of how some dev tooling handles incremental rebuilds. The failure shows up specifically in a clean production build, which scans and purges from scratch, with nothing left over from earlier development sessions to accidentally mask the problem.

The Fix: Always Write Complete Class Names, Literally

function StatusBadge({ status }: { status: 'success' | 'warning' | 'error' }) {
  const styles = {
    success: 'bg-green-100 text-green-800',
    warning: 'bg-yellow-100 text-yellow-800',
    error: 'bg-red-100 text-red-800',
  };

  return (
    <span className={`${styles[status]} px-2 py-1 rounded`}>
      {status}
    </span>
  );
}
Enter fullscreen mode Exit fullscreen mode

Every complete, literal class name Tailwind needs now appears as actual text somewhere in the source file, bg-green-100, bg-yellow-100, bg-red-100, text-green-800, and so on, even though the specific one that gets applied is still chosen dynamically at runtime based on status. Tailwind's scanner finds all three as literal strings during the build, generates CSS for all three, and the correct one gets applied correctly regardless of which branch of the mapping object actually executes.

The Rule That Generalizes Beyond This One Example

Tailwind only generates CSS for class names that exist as complete, literal text somewhere in your scanned source files, string concatenation and template literal interpolation don't count, no matter how confident the resulting string looks at runtime. This applies to any dynamic styling pattern, not just color variants, conditionally built spacing classes, responsive variants built from a variable, state-based classes constructed from a prop, all of them need the complete literal class name written out somewhere Tailwind's scanner can see it, usually in a lookup object exactly like the one above, rather than assembled through string interpolation.

I keep every dynamic variant mapped out explicitly like this across the component libraries in my Next.js and Tailwind templates, specifically because this exact bug is so easy to reintroduce months later when someone, often future-you, refactors a component back toward the more compact-looking interpolated version without remembering why it was written the longer way.

Get the templates: https://pixelanas.gumroad.com

Go check your own codebase for className={...${variable}...} patterns building Tailwind classes dynamically. If you find one, do a clean production build and actually look at it, don't trust how it renders in dev. Drop what you find in the comments.


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)