Securing Dynamic Classes Correctly
Tailwind strips every class at build time that it cannot find as a complete string in the source code. Dynamically composed class names disappear without a trace, and the safelist is the surgically precise solution that keeps exactly the right classes in the bundle.
Table of Contents
- 1. The purge problem: why dynamic classes disappear
- 2. How Tailwind CSS analyzes the source code
- 3. The safelist: basic configuration and simple entries
- 4. Safelist with regex patterns: securing entire class families
- 5. Variants in the safelist: responsive and state classes
- 6. Practical case: CMS backends and server-generated classes
- 7. Tailwind CSS v4: safelist in the CSS-first approach
- 8. Alternatives to the safelist: when content paths are better
- 9. Safelist strategies compared
- 10. Summary
- 11. FAQ
1. The purge problem: why dynamic classes disappear
Tailwind CSS is built for a lean CSS output. In production builds, the framework scans all configured source files for Tailwind classes and removes every class that it does not find there as a complete class string. This procedure, internally called content scanning, reduces typical Tailwind CSS files from several megabytes down to a few kilobytes. What gets overlooked in the process are dynamically composed classes, where the complete class name is only assembled at runtime.
A typical example: in a Vue or React component, a color is read from a prop or from an API response and composed into a Tailwind class string, for instance 'bg-' + color + '-500'. At compile time, Tailwind only sees the strings 'bg-' and '-500', but never the complete class bg-red-500 or bg-blue-500. The result: the production build is missing exactly the classes that are needed at runtime. The component works fine in development with JIT watch mode, but not in deployment. The Tailwind CSS safelist is the precise solution to this problem.
2. How Tailwind CSS analyzes the source code
Tailwind analyzes source files with a simple but effective mechanism: it looks for complete class strings that match a Tailwind class format. The scanner is not a real HTML or JavaScript parser. It splits the source text into tokens, character sequences delimited by whitespace or special characters, and checks each token against the known Tailwind classes. This works well for static classes in HTML attributes, class names in strings, and template literals that contain complete classes.
What the scanner cannot do: it does not follow variable assignments. const cls = prefix + '-red-500' never produces a complete match for bg-red-500. The same applies to classes that come from JSON configuration files, are loaded from a database, or are generated by a CMS backend. In all these scenarios, the Tailwind CSS safelist is the right tool. It registers classes explicitly, without requiring them to exist as a complete string in the source code.
3. The safelist: basic configuration and simple entries
The safelist is configured in the root object of tailwind.config.js. It accepts an array made up of simple strings (individual classes) or objects with a regex pattern and optional variants. Simple string entries are the most direct form: every string in the safelist is always included in the build, independent of content scanning. This is the right choice when the set of dynamic classes is manageable and stable.
Simple safelist entries have a clear downside: they scale poorly. Anyone who wants to secure all color utilities for all background colors would have to add hundreds of strings. This is where regex patterns come in. Before switching to regex, though, it is worth checking whether the dynamic classes could instead be written as complete strings in a lookup table in the source code, since that is the cleanest solution and requires no safelist at all.
// tailwind.config.js - Basic safelist configuration
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{html,js,ts,vue,jsx,tsx}',
'./resources/views/**/*.blade.php',
],
safelist: [
// Simple string entries - always included regardless of content scan
'bg-red-500',
'bg-blue-500',
'bg-green-500',
'text-white',
'font-bold',
// Useful for status indicators from API responses
'border-red-400',
'border-yellow-400',
'border-green-400',
// Complete animation classes often missed by scanner
'animate-spin',
'animate-pulse',
'animate-bounce',
],
theme: {
extend: {},
},
plugins: [],
}
A common trap when getting started with the Tailwind CSS safelist: classes with slash syntax for opacity are parsed differently. bg-black/50 is a valid Tailwind class string, but it must appear as an individual string in the safelist. The same applies to arbitrary values in square brackets, bg-[#1a1a1a] is a separate class that is not covered by a simple regex pattern. For such edge cases, an explicit string entry is always the safest solution.
4. Safelist with regex patterns: securing entire class families
When an entire family of Tailwind classes is used dynamically, for example all background colors across all shades, a regex pattern in the safelist is the efficient solution. The pattern format is an object with the key pattern, which contains a regular expression. Tailwind checks every known utility class against this pattern and includes all matches in the build. The pattern matches against the complete class name, not against substrings.
Regex patterns in the Tailwind CSS safelist must be formulated carefully. A pattern that is too broad, such as /bg-/, would include every class that contains the string "bg-", meaning bg-transparent, bg-inherit, bg-current, and all color classes. That produces needlessly large CSS files. A precise pattern such as /^bg-(red|blue|green|yellow|purple)-(100|200|300|400|500|600|700|800|900)$/ includes exactly the classes needed and nothing more. The art lies in the balance between precision and completeness.
// tailwind.config.js - Regex patterns in the safelist
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js,ts,vue}'],
safelist: [
// All background colors for a dynamic color picker component
{
pattern: /^bg-(slate|gray|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(100|200|300|400|500|600|700|800|900)$/,
},
// Text colors matching the same palette
{
pattern: /^text-(slate|gray|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(100|200|300|400|500|600|700|800|900)$/,
},
// Border colors for status badges from API
{
pattern: /^border-(red|yellow|green|blue)-(300|400|500)$/,
},
// Ring colors for focus states driven by a theme config
{
pattern: /^ring-(sky|indigo|violet)-(400|500|600)$/,
},
// Grid column spans for a dynamic layout engine
{
pattern: /^col-span-(1|2|3|4|5|6|7|8|9|10|11|12)$/,
},
],
theme: { extend: {} },
plugins: [],
}
5. Variants in the safelist: responsive and state classes
Regex patterns in the safelist cover only the base class by default, without responsive prefixes such as sm:, md:, lg: and without state variants such as hover:, focus: or dark:. If dynamic classes are also used with these variants, the variants must be explicitly specified in the pattern object under the key variants as an array. Tailwind then generates the combination of every match of the pattern with every specified variant.
The variants feature of the Tailwind CSS safelist has an important side effect on bundle size: a combination of a pattern that matches 50 classes with four variants produces 200 CSS rules. With a pattern that matches all colors across all shades and is secured with responsive and hover variants, thousands of classes can quickly result. It is therefore important to specify variants only for the combinations that are actually needed dynamically, and to verify the result with npx tailwindcss --dry-run or a bundle analysis.
// tailwind.config.js - Variants in safelist patterns
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js,ts,vue}'],
safelist: [
// Background colors with hover and focus variants (e.g. dynamic button colors)
{
pattern: /^bg-(sky|indigo|violet|rose)-(400|500|600)$/,
variants: ['hover', 'focus', 'active'],
},
// Text colors with dark mode variant
{
pattern: /^text-(slate|gray)-(600|700|800|900)$/,
variants: ['dark'],
},
// Responsive grid columns - different column count per breakpoint
{
pattern: /^grid-cols-(1|2|3|4|6|12)$/,
variants: ['sm', 'md', 'lg', 'xl'],
},
// Opacity for dynamically shown/hidden elements
{
pattern: /^opacity-(0|25|50|75|100)$/,
variants: ['group-hover', 'hover'],
},
],
theme: { extend: {} },
plugins: [],
}
6. Practical case: CMS backends and server-generated classes
A classic use case for the Tailwind CSS safelist is the CMS backend: editors can choose colors, spacing, or layouts in a WYSIWYG editor or a field configuration dialog. This choice is stored as a database value and output at runtime in PHP, Twig, or Blade as a Tailwind class string. The possible class combinations are known, but they do not exist in any static source file that Tailwind could scan.
In Magento 2 with Hyvä Themes, the same pattern occurs with product attributes: a color family is stored as an attribute value, and the template composes a Tailwind class string from it. Without a safelist, the rendering works correctly in the development environment with watch mode, because JIT generates every class string on demand there. In the production build, the classes are missing. The best solution in such scenarios: create a separate TypeScript or JavaScript file that contains all possible classes as complete strings in an object or array, and include this file in Tailwind's content paths. This is cleaner than a safelist because it documents the possible classes and makes them type-safe.
7. Tailwind CSS v4: safelist in the CSS-first approach
Tailwind CSS v4 moves away from JavaScript configuration in tailwind.config.js toward a CSS-first approach: configuration happens in the main CSS file with @theme blocks. In this new approach, the safelist can be solved via an @source directive combined with an unsafe inline pattern. The underlying mechanics stay the same: Tailwind must be told explicitly which classes it cannot infer from source files but should always generate.
In v4 there is the directive @source inline("..."), which treats a string or a glob pattern directly as a source. Anyone who wants to secure all red background classes writes @source inline("{bg-red-100,bg-red-200,...,bg-red-900}"). It feels different from the JavaScript configuration, but achieves the same goal. For projects still on Tailwind CSS v3, the tailwind.config.js safelist with regex patterns remains the recommended path. The switch to v4 should not happen solely because of safelist syntax, since the migration has far bigger effects on the overall setup.
8. Alternatives to the safelist: when content paths are better
The Tailwind CSS safelist is not always the best solution. When the dynamic classes come from a defined set that lives in a configuration file or a mapping object in the source code, it is better to add that file to the content configuration. Tailwind then scans the file and automatically finds all complete class strings, without a safelist needing to be maintained.
The principle: a central file src/config/tailwind-classes.ts contains an object that lists all possible dynamic classes as complete strings. This file is registered in content. Tailwind scans it, finds all classes, and keeps them in the build. The advantage over the safelist: the classes are documented, type-safe, and refactorable. One drawback: for very large sets or for externally defined classes (CMS, database), this strategy is not practical, and there the safelist remains the right tool.
9. Safelist strategies compared
The right strategy for dynamic Tailwind classes depends on the use case. The following table shows the main options and when each one fits.
| Strategy | Works for | Tailwind CSS safelist needed? | Recommendation |
|---|---|---|---|
| Complete strings in the source code | Lookup tables, mapping objects | No | Best solution, always prefer it |
| Content path on config file | Classes in separate TS/JS file | No | Clean, documented, type-safe |
| Safelist with strings | Few, stable dynamic classes | Yes | Good for small, known sets |
| Safelist with regex pattern | Entire class families | Yes | Efficient, watch bundle size |
| Safelist + variants | Responsive/state for dyn. classes | Yes, with care | Avoid bundle explosion |
The Tailwind CSS safelist is not a safety net that should be enabled across the board. It solves a specific problem and should be formulated as precisely as possible. A pattern that is too broad has a direct impact on the CSS bundle size and therefore on the website's load time. The ideal state is a codebase structured so that complete class strings are always present in statically analyzable source code, in which case no safelist is needed at all.
Mironsoft
Tailwind CSS setup, build optimization and frontend architecture
Want to optimize a Tailwind build with dynamic classes?
We analyze your Tailwind build, identify missing classes, and configure a precise safelist strategy, without unnecessary bundle bloat and with clean documentation of the dynamic classes.
Build analysis
Identify missing classes in the production build and document the causes
Safelist configuration
Precise regex patterns and variants for all dynamic classes
Bundle optimization
Minimize CSS output and restrict it to actually used classes
10. Summary
The Tailwind CSS safelist solves a specific and frequently occurring problem: classes that are only composed at runtime are missing from the production build because the content scanner cannot find them as complete strings. The safelist registers these classes explicitly, either as individual strings for small, stable sets or as regex patterns for entire class families. Variants in the pattern object secure responsive and state variants, but must be used with care so as not to needlessly increase bundle size.
The best strategy for dynamic Tailwind classes is always to have complete class strings in statically analyzable source code, in a lookup table, a mapping object, or a configuration file registered in Tailwind's content path. When that is not possible, for CMS-generated classes, database values, or externally configured layouts, the safelist is the precise tool. Tailwind CSS v4 solves the same problem with @source inline() directives in the CSS-first approach. The core rule stays the same in both versions: never compose class names from partial strings at runtime without making the complete class string statically available somewhere.
Tailwind CSS Safelist - The Essentials at a Glance
Cause of the problem
Dynamically composed classes ('bg-' + color) are not recognized as complete Tailwind classes by the content scanner and are removed in the build.
Basic safelist form
In tailwind.config.js: safelist: ['bg-red-500', ...] for individual classes or { pattern: /regex/, variants: ['hover'] } for families.
Best alternative
Store complete class strings in a static lookup file and register its path in content, cleaner than a safelist.
Tailwind v4
@source inline("{bg-red-100,...}") in the main CSS file is the equivalent of the JavaScript safelist in v4's CSS-first approach.
11. FAQ: Tailwind CSS Safelist and Dynamic Classes
1Why are dynamic classes missing from the production build?
'bg-' + color) as complete strings in the source code and removes them. Safelist or a lookup table solve the problem.2Safelist vs. content paths: what is the difference?
3Regex in the safelist: how do I write a precise pattern?
/^bg-(red|blue|green)-(400|500|600)$/ matches exactly the named classes. Without anchors (^ and $), substrings of other classes get matched, so always set anchors.4Hover and responsive variants in the safelist?
{ pattern: /.../, variants: ['hover', 'sm', 'lg'] } generates all combinations. Only specify variants that are actually needed dynamically, since every variant multiplies the class count.5Bundle size with a broad safelist?
npx tailwindcss -o out.css to check.6Safelist in Tailwind CSS v4?
@source inline("{bg-red-100,...}") in the CSS file. No more tailwind.config.js, the CSS-first approach takes over the configuration entirely.7It works in development but not in the build, why?
8Lookup table instead of safelist, how do I implement it?
src/config/tw-classes.ts with complete class strings, register it in content: ['./src/config/tw-classes.ts']. Tailwind scans it and keeps all strings.9Securing CMS-generated classes?
10Which classes are present after the build?
npx tailwindcss -o output.css --minify, then grep 'bg-red-500' output.css. If the class is missing, the safelist or lookup table is not yet configured correctly.