You write class="bg-{{ $status->color }}-500" in a Blade template, it looks perfect on your machine, then you ship it and the colour is gone. The utility never made it into the compiled stylesheet. In Tailwind v3 you'd reach for the safelist array in tailwind.config.js — but v4 moved configuration out of JavaScript entirely, and the safelist key went with it. Here's how to safelist dynamic classes in Tailwind v4 with @source inline().
Reproduce the dynamic class purge#
Tailwind never executes your PHP. It treats every source file as plain text, scans for anything that looks like a class name, and generates CSS only for those literal tokens. An interpolated value like bg-{{ $color }}-500 isn't a token it can resolve — the real string bg-red-500 only exists at runtime, long after the build has finished.
{{-- resources/views/components/status-badge.blade.php --}}
@props(['status'])
{{-- $status->color is 'red', 'green', or 'amber' from the database --}}
<span class="inline-flex items-center rounded-full px-2 py-1 text-sm font-medium
bg-{{ $status->color }}-100 text-{{ $status->color }}-700">
{{ $status->label }}
</span>
Tailwind sees the literal fragments bg-, -100, text- and -700, plus the Blade markers — but never the complete bg-red-100. So it generates nothing for it. The badge looks fine locally only if bg-red-100 happens to appear as a whole string somewhere else in your project. The day that other reference disappears, or you run a clean production build, the colour vanishes.
Add the @source inline() directive#
The fix lives in your CSS entry point, not a config file. Open resources/css/app.css — the file with @import "tailwindcss" — and register each utility you need Tailwind to emit unconditionally with @source inline().
/* resources/css/app.css */
@import "tailwindcss";
/* Force these into the bundle even when no literal class exists in source */
@source inline("bg-red-100");
@source inline("bg-green-100");
@source inline("bg-amber-100");
@source inline("text-red-700");
@source inline("text-green-700");
@source inline("text-amber-700");
This is the direct replacement for v3's safelist array. It's the same CSS-first shift Tailwind made with custom utilities via the @utility directive — configuration that used to live in JavaScript now sits in your stylesheet.
Safelist class ranges with brace expansion#
Listing every utility on its own line gets tedious fast. The string inside @source inline() is brace-expanded — the same syntax Bash uses — so you can describe a whole matrix of classes in a single line.
@import "tailwindcss";
/* 3 colours x 3 shades x {no variant, hover:} = 18 utilities */
@source inline("{hover:,}bg-{red,green,amber}-{100,500,700}");
Each comma-separated group multiplies out against the others. The leading {hover:,} is the idiom for "with and without the hover variant": the empty slot after the comma generates the bare class, the hover: slot generates the variant. For a full shade ramp, swap the explicit shade list for a numeric range:
/* 50, 100, 200 ... 900, 950 — the whole red ramp, plus hover variants */
@source inline("{hover:,}bg-red-{50,{100..900..100},950}");
{100..900..100} means "from 100 to 900 in steps of 100". This is the exact pattern the Tailwind docs use, and it pairs naturally with the theme colour tokens you define in CSS.
Rebuild and verify the generated CSS#
Safelisting happens at build time, so nothing changes until you recompile. Run the production build and prove the previously-missing utility is actually in the output before you trust it.
npm run build
# Grep the compiled stylesheet for the class that was disappearing
grep -o 'bg-red-100' public/build/assets/*.css
If grep echoes the class name back, it's in the bundle and your badge will render in production. During development, npm run dev picks up edits to app.css on the next rebuild, so there's no separate safelisting step to run.
Gotchas and Edge Cases#
Brace expansion is a loaded gun. A pattern like {hover:,focus:,}bg-{red,green,blue,amber,rose}-{50,{100..900..100},950} emits several hundred rules from one line, and every one of them ships to every visitor. Only safelist the colours and shades you genuinely render.
@source inline() arrived in Tailwind v4.1. On v4.0 the directive is silently ignored, so if it seems to do nothing, check your installed version with npm ls tailwindcss before debugging anything else.
Reach for safelisting only when the class set is genuinely dynamic — driven by user input or a database column you don't control. When the options are known and finite, map them to complete class names in PHP instead. It's more maintainable, it keeps the bundle minimal, and it's what the Tailwind team recommends — the same "define it in code, not config" philosophy behind CSS-first dark mode in v4.
// A match() that returns whole class strings keeps the scanner happy:
// Tailwind sees complete literals, so no safelist is required.
$classes = match ($status->color) {
'red' => 'bg-red-100 text-red-700',
'green' => 'bg-green-100 text-green-700',
'amber' => 'bg-amber-100 text-amber-700',
};
One last trap: don't confuse the directive forms. @source "../resources/views" registers a path to scan. @source inline("…") injects literal utilities. And @source not inline("…") stops a class from being generated even when it is detected in your markup — useful for trimming utilities you never want out of a third-party component library. They look alike and do very different things.
Wrapping Up#
Default to static class maps, and keep @source inline() for the genuinely data-driven cases — being surgical with brace expansion so you don't ship a bloated stylesheet. After every change, grep the build to confirm the class is really there. With the dynamic-class problem solved, the natural next step is making those components adapt to their context with Tailwind v4 container queries.
FAQ#
How do I safelist classes in Tailwind v4?
Add a @source inline() directive to your main CSS file — the one with @import "tailwindcss". Pass it the literal utility you want generated, for example @source inline("bg-red-500"), and Tailwind emits that class even when it never appears in your markup. This is the v4 replacement for the safelist array that used to live in tailwind.config.js.
Why are my dynamic Tailwind classes not working?
Tailwind scans your source files as plain text and only generates classes it finds as complete literal strings. A class assembled at runtime — like bg-{{ $color }}-500 in Blade or bg-${color}-500 in JavaScript — never exists as a literal, so Tailwind has nothing to match and skips it. Either safelist the classes with @source inline() or map your dynamic values to complete class names in code.
Does Tailwind v4 still support the safelist config option?
No. The safelist, corePlugins and separator options from the JavaScript config are not supported in Tailwind v4. The official replacement for safelisting is the @source inline() directive, which you write in CSS rather than in a config file. Tailwind v4 can still load a legacy tailwind.config.js through @config, but the safelist key inside it is ignored.
How do I use @source inline with brace expansion?
Wrap the variable parts of the class in braces and Tailwind expands every combination, the same way Bash does. For example, @source inline("{hover:,}bg-{red,green,blue}-500") generates the bare and hover: variants of three background colours in one line. For numeric shade ramps, use a range like bg-red-{50,{100..900..100},950} to cover 50 through 950 without listing every value by hand.
How do I stop Tailwind from purging classes used in Blade variables?
Tailwind isn't really purging them — it never generated them, because the interpolated string is not a literal it can see. The most robust fix is to avoid interpolated class fragments altogether: use a match() or a lookup array in your component that returns complete class strings like bg-red-100 text-red-700. When the value set is truly dynamic, safelist the possibilities with @source inline() instead.