Tailwind CSS in Production · Missing and Wrong Styles
The Badge That Lost Its Colour
Status badges rendered grey in production because their colour classes were built from string fragments that Tailwind's scanner never saw.
Scenario
Example team: an order dashboard shows a coloured badge per status. A developer refactors the badge into a component that takes a color prop and builds the class name: bg-${color}-100 text-${color}-700. It looks fine locally, where an old build still has the classes cached.
What you see in production
After the next deploy every badge is plain grey text. Nothing is wrong in the HTML: the classes are there (bg-amber-100), but the CSS file contains no rule for them.
The naive code
function Badge({ color, children }) {
return <span className={`rounded-full px-2 py-0.5 bg-${color}-100 text-${color}-700`}>{children}</span>;
}
Reproduce it
Tailwind generates CSS for class names it finds as plain text in your source files. A small model of that scan:
import re
source = """
function Badge({ color, children }) {
return <span className={`rounded-full px-2 py-0.5 bg-${color}-100 text-${color}-700`}>{children}</span>;
}
const PAID = "bg-green-100 text-green-700"; // a complete class string
"""
candidates = set(re.findall(r"[\w:/.\-\[\]]+", source))
for wanted in ["bg-amber-100", "text-amber-700", "bg-green-100", "text-green-700", "rounded-full"]:
print(f"{wanted:15} {'found -> CSS generated' if wanted in candidates else 'NOT found -> no CSS'}")
print("the scanner only sees fragments like:", sorted(c for c in candidates if c.startswith(("bg-", "text-"))))
Output
bg-amber-100 NOT found -> no CSS text-amber-700 NOT found -> no CSS bg-green-100 found -> CSS generated text-green-700 found -> CSS generated rounded-full found -> CSS generated the scanner only sees fragments like: ['bg-', 'bg-green-100', 'text-', 'text-green-700']
Root cause
Tailwind doesn't run your JavaScript. It reads files as text and keeps anything that looks like a class. bg-${color}-100 is split into fragments, so bg-amber-100 is never generated unless it appears in full somewhere else.
The fix
Map each variant to complete class names:
<!-- const STATUS = { paid: "bg-green-100 text-green-700", pending: "bg-amber-100 text-amber-700", overdue: "bg-red-100 text-red-700" } -->
<div class="flex gap-2 p-6">
<span class="rounded-full bg-green-100 px-2.5 py-0.5 text-sm font-medium text-green-700">Paid</span>
<span class="rounded-full bg-amber-100 px-2.5 py-0.5 text-sm font-medium text-amber-700">Pending</span>
<span class="rounded-full bg-red-100 px-2.5 py-0.5 text-sm font-medium text-red-700">Overdue</span>
</div>
If values really come from data (a CMS or database), safelist them in CSS: @source inline("bg-{red,amber,green}-100");.
Trade-offs
- A mapping object is a little more code, but it's searchable, type-checkable and documents the allowed variants.
- Safelisting adds CSS even when unused; keep the list small.
Detect it in production
- Visual regression tests (screenshots) of each component variant in CI.
- A lint rule against template literals in
className(for example witheslint-plugin-tailwindcssor a custom rule).
Key points
- Tailwind finds classes by scanning source text; build-time, not runtime.
- Never assemble class names from fragments; map to complete strings.
- Safelist with
@source inline()only for genuinely dynamic data.
Practice questions
Why does className={`bg-${color}-500`} produce no styles in a production build?
Answer
Tailwind scans source files as plain text and only generates CSS for class names written in full. The interpolated string never contains bg-red-500, so no rule is generated.
How do you safelist classes that come from a database?
Answer
In v4, add @source inline("…") to your CSS (brace expansion is supported, e.g. bg-{red,green}-100). In v3 it was the safelist option in tailwind.config.js.
Open in the app to mark lessons complete, take notes and track your progress.