Tailwind CSS Safelist: Securing Dynamic Classes Correctly
AI generated
</>
tw
Tailwind CSS · Safelist · Build Optimization · JIT
Tailwind CSS Safelist:
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.

12 min read Safelist · Regex Patterns · Variants · tailwind.config.js Tailwind CSS v3 · v4 · Vite · PostCSS

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?
Tailwind does not find dynamically composed classes (e.g. '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?
Content paths let Tailwind scan files and find complete strings. The safelist registers classes explicitly, independent of scanning. Prefer a lookup file in content.
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?
200 classes x 4 variants = 800 CSS rules. Manageable with gzip/brotli, but always choose the most precise pattern. Use npx tailwindcss -o out.css to check.
6Safelist in Tailwind CSS v4?
In 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?
JIT watch mode generates classes on demand. The production build scans statically and removes classes it does not find. The development environment hides the problem.
8Lookup table instead of safelist, how do I implement it?
Create a file 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?
When classes come from the database, the safelist is often the only option. A regex pattern that covers all possible CMS classes, plus a regular check for new classes.
10Which classes are present after the build?
Run 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.