The problem: Utility classes built dynamically from PHP variables (like bg-{{ $color }}-500) work fine in local dev but disappear entirely after running a production build.
Environment: Laravel 11, Tailwind CSS 3.x, Vite 5, Blade templates
The Error
No JS/build error thrown.
Element renders with class="bg-red-500" in the DOM,
but computed styles show no background color applied
in the production build. Works fine in `npm run dev`.
Why It Happens
Tailwind doesn't actually watch your rendered HTML output at build time — it statically scans your source files as plain text looking for complete, literal class name strings, then only generates CSS for the ones it finds. In dev mode with the JIT engine watching files, this can appear to work because of how the class ends up written elsewhere too. But the moment you build a string like "bg-{$color}-500", Tailwind's scanner never sees the literal string bg-red-500 anywhere in your source — it only sees the fragments bg- and -500 with a PHP variable in between, which it can't resolve statically because it isn't a real interpreter, just a text scanner.
The result: the class technically appears in your rendered HTML (because PHP concatenated it correctly at runtime), but no matching CSS rule exists in the compiled stylesheet, because Tailwind never generated it in the first place.
The Fix
The most reliable fix is avoiding dynamic string concatenation entirely — write out the full class names literally in a lookup map so Tailwind's scanner can see every complete string:
- <div class="bg-{{ $color }}-500">
+ @php
+ $colorClasses = [
+ 'red' => 'bg-red-500',
+ 'green' => 'bg-green-500',
+ 'blue' => 'bg-blue-500',
+ ];
+ @endphp
+ <div class="{{ $colorClasses[$color] ?? 'bg-gray-500' }}">
If the value truly can't be enumerated ahead of time (rare, but happens with user-generated theming), add a safelist entry in your Tailwind config so those specific classes are always generated regardless of whether the scanner sees them as literal text:
module.exports = {
content: ['./resources/**/*.blade.php'],
safelist: [
'bg-red-500',
'bg-green-500',
'bg-blue-500',
],
}
For a larger, pattern-based set of dynamic values, safelist supports regex patterns too:
safelist: [
{ pattern: /bg-(red|green|blue|yellow)-(400|500|600)/ },
]
Step-by-Step
- Find every place a Tailwind class is built from string concatenation or a PHP variable rather than written literally.
- Prefer converting these to a lookup map/array with full literal class strings — this is the safest, most maintainable fix.
- For genuinely dynamic/unknowable values, add explicit classes or regex patterns to
safelistintailwind.config.js. - Rebuild:
npm run build. - Inspect the compiled CSS output (
public/build/assets/app-*.css) and search for the expected class to confirm it's now present. - Test the affected component in production build mode (not just dev) since this bug only appears after a real build, not during JIT watch mode.
Edge case: If the dynamic classes come from a database (e.g. an admin panel lets users pick a "theme color" stored as a string), a code deploy alone won't fix existing records — you'll need to either constrain the admin UI to only allow safelisted values going forward, or run a data migration to normalize existing values to match your safelist exactly.
Still seeing missing styles after safelisting? Reach out on the Contact page and I'll help debug your specific class patterns.