How To Fix Tailwind Purge Removing Dynamic Classes In React?

You build a React component. You pass a color prop. You write bg-${color}-500. You run your app. The button shows no background color at all. Sound familiar? This is one of the most common Tailwind problems React developers face.

Your dynamic classes vanish during the build. The styles work in development sometimes, then disappear in production. It feels like a bug, but it is actually how Tailwind is built to work.

The good news is that this problem has clear fixes. This guide explains why it happens and gives you step by step solutions you can use today. By the end, you will know exactly how to keep your dynamic classes alive.

Key Takeaways

  • Tailwind scans your files as plain text. It never runs your JavaScript. So string interpolation like bg-${color}-500 produces no class because the full name never exists in your code.
  • Complete class names are the golden rule. Tailwind only generates CSS for class names that appear as full, unbroken strings in your source files.
  • Object maps are the cleanest fix. Map each prop value to a complete class name inside an object. This keeps everything static and readable.
  • The safelist option is your safety net. When class names truly come from runtime data, add them to the safelist in your config so they always ship.
  • Tailwind v4 changed the approach. Version 4 uses @source inline() in your CSS file instead of the old safelist array. Pick the method that matches your version.
  • Tools like clsx and tailwind merge help you stay clean. They make conditional class logic easier to manage without breaking detection.

Why Tailwind Removes Your Dynamic Classes In The First Place

Tailwind does not actually have a separate “purge” step in newer versions. Instead, it scans your project and generates only the classes it finds. This is the source of the confusion. Many people still call it purging because old versions used PurgeCSS to strip unused styles.

Here is the core truth. Tailwind treats every file as plain text. It does not parse your code or run your logic. It just looks for tokens that look like class names. When you write bg-${color}-500, the scanner sees a broken string. The full name bg-blue-500 never exists on its own. So Tailwind throws it away.

This design keeps your final CSS file tiny and fast. The trade off is that runtime class building breaks. Once you understand this, every fix below makes perfect sense.

How To Spot The Problem In Your React Project

Catching this issue early saves hours. The most common sign is a style that works in development but breaks in production. Another sign is a class that simply never applies, even in dev. Your component renders, but the color, spacing, or size is missing.

Open your browser dev tools. Inspect the element. If the class name is in the HTML but the matching CSS rule does not exist, you found it. The class shipped to the browser, but Tailwind never built the rule.

Try this quick test. Search your codebase for backticks or plus signs near className. Look for patterns like text-${size} or "bg-" + color. These are your suspects. Any class name built by combining strings is a risk. Once you list them, you can decide which fix fits each case best.

Solution One: Always Use Complete Class Names

This is the rule Tailwind itself recommends first. Never split a class name into pieces. Write the full, unbroken string every time. The scanner can only find what is fully visible.

Look at this broken example:

<div className={`text-${error ? 'red' : 'green'}-600`}></div>

Here text-red-600 and text-green-600 never appear as complete strings. Fix it like this:

<div className={error ? 'text-red-600' : 'text-green-600'}></div>

Now both full names sit right there in the file. Tailwind finds them and builds the CSS.

Pros: This needs no config changes. It is the cleanest and most reliable method. It works in every Tailwind version.

Cons: It can feel repetitive with many variants. Long ternary chains get hard to read. For a handful of states though, it is the best choice you have.

Solution Two: Map Props To Static Class Names With Objects

When you have several prop driven variants, the object map shines. You create an object where each key holds a complete class string. Then you look up the value using the prop. Tailwind sees every full name sitting in the object.

Here is a clean button example:

function Button({ color, children }) {
  const colorVariants = {
    blue: "bg-blue-600 hover:bg-blue-500 text-white",
    red: "bg-red-500 hover:bg-red-400 text-white",
    yellow: "bg-yellow-300 hover:bg-yellow-400 text-black",
  };
  return <button className={`${colorVariants[color]} rounded px-4 py-2`}>{children}</button>;
}

The prop only picks which full string to use. It never builds a name itself. This is the pattern the official docs prefer.

Pros: It is readable, scalable, and fully static. You can map different shades per variant, which is a nice bonus.

Cons: You must write each combination by hand. For dozens of dynamic options, the object grows large.

Solution Three: Use The Safelist In Your Tailwind Config

Sometimes class names truly come from a database or API. You cannot know them at build time. This is where the safelist saves you. It tells Tailwind to always include certain classes, even if they never appear in your code.

In Tailwind v3, open tailwind.config.js and add a safelist array:

const usedColors = ['blue', 'red', 'green'];

module.exports = {
  safelist: usedColors.map((c) => `text-${c}-600`),
  // rest of your config
};

You can build the list with a loop. For example, [2, 4].map((n) => 'line-clamp-' + n) gives you line-clamp-2 and line-clamp-4. Every name in this array ships no matter what.

Pros: It handles true runtime classes that no static method can. It is flexible and scriptable.

Cons: It increases your CSS file size. Overusing it defeats the purpose of small output. Keep the list tight.

Solution Four: Safelist With Patterns And Variants

The safelist supports more than plain strings. You can use a pattern object to match a range of classes. This helps when you need many shades or sizes at once. A regex pattern keeps your config short.

In Tailwind v3, a pattern looks like this:

module.exports = {
  safelist: [
    {
      pattern: /bg-(red|green|blue)-(100|500|900)/,
      variants: ['hover', 'focus'],
    },
  ],
};

This single rule generates background colors for three colors across three shades. The variants key adds hover: and focus: versions too. That is a lot of coverage from a few lines.

Pros: It is compact and powerful. You avoid writing huge lists by hand. It pairs perfectly with design systems.

Cons: Broad patterns can bloat your CSS fast. A loose regex may generate hundreds of unused rules. Test the output size after you add one.

Solution Five: The Tailwind v4 Way With @source inline()

Tailwind version 4 changed how safelisting works. The old safelist config array is gone in the new setup. Instead, you write directives directly in your CSS file. This feels different but it is just as capable.

To force a class to always generate, use @source inline():

@import "tailwindcss";
@source inline("bg-blue-500");

You can add variants and ranges too. This example builds red backgrounds from 100 to 900 with hover support:

@source inline("{hover:,}bg-red-{50,{100..900..100},950}");

The braces use brace expansion, so one line creates many classes. This is the modern replacement for pattern based safelists.

Pros: It lives next to your styles, which keeps things tidy. The range syntax is fast to write.

Cons: The syntax is new and takes time to learn. It only works in v4 and later. Check your version before you use it.

Solution Six: Register External Sources Tailwind Misses

Sometimes the missing classes are not yours. They live inside a component library you installed. Tailwind ignores node_modules by default. So classes from a third party UI kit never get scanned, and they vanish.

In Tailwind v4, you fix this with the @source directive in your CSS:

@import "tailwindcss";
@source "../node_modules/@yourcompany/ui-lib";

This tells Tailwind to scan that folder for class names. Now the library styles ship with your build. This is common when you use shared design systems across projects.

Pros: It solves a sneaky problem that confuses many teams. It keeps external components styled correctly.

Cons: Scanning extra folders adds build time. Pointing at a huge package can slow you down. Aim at the exact path you need.

Solution Seven: Use clsx And tailwind-merge For Clean Logic

Conditional classes get messy fast. Long ternary strings hurt readability. Tools like clsx and tailwind-merge keep your logic clean. They do not fix detection by magic, but they help you write full class names safely.

The clsx library joins class names based on conditions:

import clsx from 'clsx';

const className = clsx('px-4 py-2', {
  'bg-blue-500': isPrimary,
  'bg-gray-300': !isPrimary,
});

Notice every name is complete. That is the key to keeping detection working. The tailwind-merge tool then removes conflicting classes, so px-2 px-4 becomes just px-4.

Pros: Your conditional code stays readable and clean. Merging prevents style conflicts in reusable components.

Cons: It adds a small dependency to your project. It does not save you from string interpolation mistakes. You still must write full names.

Solution Eight: Try Class Variance Authority For Variant Heavy Components

When a component has many variants, sizes, and states, the object map grows huge. Class Variance Authority, often called cva, organizes this beautifully. It lets you define variants in a structured way while keeping every class name complete.

Here is a short example:

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

const button = cva('rounded font-medium', {
  variants: {
    color: { blue: 'bg-blue-500', red: 'bg-red-500' },
    size: { sm: 'px-2 py-1', lg: 'px-6 py-3' },
  },
});

You call button({ color: 'blue', size: 'lg' }) and get a clean string. Every class lives statically inside the config, so Tailwind detects them all.

Pros: It scales well for complex design systems. It keeps variant logic in one tidy place.

Cons: There is a learning curve for the API. It may be overkill for small projects with few variants.

How To Test That Your Fix Actually Works

A fix means nothing until you confirm it. Always test your production build, not just your dev server. Dynamic class problems often hide in development and only appear after you build.

Run your build command, usually npm run build. Then preview the output. Open the page and inspect the element again. The class should now have a matching CSS rule. If it does, you solved it.

You can also search your generated CSS file directly. Open the output file and search for the class name, like bg-blue-500. If it is there, Tailwind generated it. This single check saves you from shipping broken styles.

For safelist users, watch your CSS file size before and after. A sudden jump means your pattern is too broad. Trim it down until you only include what you truly need.

Best Practices To Avoid This Problem Forever

Prevention beats fixing. Make full class names a habit across your whole team. Add a note in your code style guide. New developers should learn this rule on day one.

Set up a lint rule if you can. Some ESLint plugins flag dynamic Tailwind class building. An automated check catches mistakes before they reach production. This protects you better than memory ever will.

Keep your safelist short and documented. Write a comment next to each entry explaining why it exists. A bloated safelist slowly cancels the benefit of small CSS. Review it during cleanup sprints.

Finally, pick one pattern and stick with it. Use object maps for prop variants and the safelist only for true runtime data. Consistency makes your codebase easy to read and easy to debug. These small habits remove the problem for good.

Frequently Asked Questions

Why do my Tailwind classes work in development but not production?

In development, Tailwind sometimes generates a wider set of classes, so dynamic names may slip through. In production, it only ships the classes it detects as complete strings. Your interpolated class name never exists as a full string, so it gets dropped during the production build. Always test the production output.

Does Tailwind still use PurgeCSS to remove classes?

No, modern Tailwind does not use a separate PurgeCSS step. Newer versions generate only the classes they find while scanning your files. People still say “purge” out of habit. The result is the same though. Classes that never appear as full strings simply never get built.

Can I use a safelist for arbitrary values like bg-[#1da1f2]?

Yes, you can safelist arbitrary value classes too. In Tailwind v3 you add the exact string to the safelist array. In v4 you use @source inline() with the full value. Just make sure the bracket syntax matches exactly what your component renders at runtime.

What is the cleanest way to handle prop based colors in React?

The object map is the cleanest method for most cases. You map each prop value to a complete class string inside an object. The prop only selects which full string to use. This keeps everything static, readable, and fully detectable by Tailwind without needing a safelist.

Will using a safelist make my CSS file too large?

It can, if you safelist too much. Each safelisted class adds to your final file, even if no page uses it. Broad regex patterns are the biggest risk. Keep your safelist focused on only the classes that truly come from runtime data, and review it regularly.

Do I need clsx to fix the dynamic class problem?

No, clsx does not fix detection on its own. It only helps you write conditional classes cleanly while keeping each name complete. You still must avoid string interpolation. Think of it as a readability tool, not a magic solution to the purge problem itself.

Similar Posts