SuperAPI AI Directory

tailwind-errors-dynamic-classes

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

Tailwind Dynamic Class Names : The #1 Reported Issue

If a class only exists in source code as a fragment (bg-${color}-500, text-{{variant}}-600, server JSON, CMS attributes), Tailwind never generates the CSS rule. The DOM gets the class attribute, the stylesheet does not get the rule, the element renders unstyled.

Companion skills :

  • tailwind-impl-config-v4 : @source inline directive in depth
  • tailwind-impl-config-v3 : v3 safelist configuration
  • tailwind-errors-purge-issues : adjacent purge-detection failures
  • tailwind-core-design-system : prop-to-class mapping pattern

The Rule

Tailwind treats all source files as plain text and does NOT parse them as code. A class must exist as a literal complete token in some scanned file to be generated.

This rule is universal across v3 and v4. Template literals, string concatenation, runtime composition, server-injected strings, and CMS data are ALL opaque to the scanner.

<!-- NEVER works (no complete token) -->
<div class="text-{{ error ? 'red' : 'green' }}-600"></div>
<div class={`bg-${color}-500`}></div>
<div :class="`p-${size}`"></div>

<!-- ALWAYS works (complete tokens) -->
<div class="{{ error ? 'text-red-600' : 'text-green-600' }}"></div>
<div class={isError ? 'text-red-600' : 'text-green-600'}></div>

Why Dev Works But Prod Fails

In development the bundler may include the full unpurged Tailwind output (HMR re-runs the scanner on every save, picking up classes you just typed in a test). Production builds run purge once against the shipped source, and any class that never appeared as a complete token during that one scan is gone.

Some teams discover the bug after pasting a Storybook story or a markdown demo into the project ; that file briefly contained bg-red-500, the dev build cached the rule, then the file moved or was edited and the rule survived in the dev bundle. The prod build runs from a clean state and discovers no bg-red-500 token anywhere.

Decision Tree : Which Fix

Is the value bounded to a small known set
(e.g. variant: 'primary' | 'secondary' | 'danger')?
├── YES : Fix 1 (static map)
└── NO : Is the value a numeric range from a CMS / API?
    ├── YES (e.g. user-picked shade 50-950) :
    │   ├── v4 : Fix 4 (@source inline with range)
    │   └── v3 : Fix 3 (safelist with regex pattern)
    └── NO : truly arbitrary colour / size string :
        Fix 2 (inline style)

Fix 1 : Static Map (PREFERRED)

ALWAYS use this when the value is bounded to a known set. It is zero-runtime, fully type-checkable, and the static analyser sees every class.

function Button({ color, children }) {
  const variants = {
    blue: 'bg-blue-600 hover:bg-blue-500 text-white',
    red: 'bg-red-500 hover:bg-red-400 text-white',
    green: 'bg-emerald-600 hover:bg-emerald-500 text-white',
  }
  return <button className={variants[color]}>{children}</button>
}

Why it works : every class string ('bg-blue-600 hover:bg-blue-500 text-white') is a complete literal token in the source file. The Tailwind scanner reads it verbatim.

TypeScript narrows further :

type Variant = keyof typeof variants
function Button({ color }: { color: Variant }) { /* ... */ }

NEVER write className={`bg-${color}-500`} and expect a fix later. The static map is the fix.

Fix 2 : Inline Style for Arbitrary Values

When the value is truly arbitrary (a hex code from a user colour picker, a CSS length from a CMS field), NEVER try to express it as a Tailwind class. Use the style attribute :

function ColorBlock({ hex }) {
  return <div style={{ backgroundColor: hex }} className="rounded-md p-4" />
}

function CustomSpacing({ pixels }) {
  return <div style={{ marginBlock: `${pixels}px` }} />
}

Inline style bypasses Tailwind entirely. The class system covers the design system ; truly free-form values bypass it.

Hybrid pattern (Tailwind structure + inline runtime value) :

<button
  className="rounded-md px-4 py-2 font-medium text-white transition"
  style={{ backgroundColor: userTheme.primary }}
>
  Tap
</button>

Fix 3 : v3 Safelist (Bounded but Many Classes)

For projects on v3 where the set is too large to inline as a map (a CMS that emits any shade between 50 and 900 for any of fifteen colours), use the safelist array in tailwind.config.js.

Literal strings :

module.exports = {
  content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
  safelist: [
    'bg-red-500',
    'bg-blue-500',
    'text-3xl',
  ],
}

Regex pattern :

module.exports = {
  safelist: [
    {
      pattern: /bg-(red|green|blue|amber|purple)-(50|100|500|700|900)/,
    },
  ],
}

Pattern with variants array :

module.exports = {
  safelist: [
    {
      pattern: /bg-(red|green|blue)-(100|200|300)/,
      variants: ['hover', 'focus', 'lg', 'lg:hover'],
    },
  ],
}

CRITICAL RULE : patterns can only match BASE utility names. NEVER include variants inside the regex itself :

// WRONG : variants in regex never match
{ pattern: /hover:bg-(red|green)-500/ }

// RIGHT : variants in the variants array
{
  pattern: /bg-(red|green)-500/,
  variants: ['hover'],
}

Fix 4 : v4 @source inline (PREFERRED for v4)

v4 replaces the safelist config option with the @source inline CSS directive. The grammar supports brace expansion AND numeric ranges, generating the Cartesian product in one line.

Single class :

@import "tailwindcss";
@source inline("underline");

With variants via brace expansion :

@source inline("{hover:,focus:,}underline");

The empty alternative in {hover:,focus:,} (note the trailing comma) includes the bare class WITHOUT a variant. So this generates : underline, hover:underline, focus:underline.

Numeric ranges :

@source inline("bg-red-{50,{100..900..100},950}");

{100..900..100} generates 100, 200, 300, 400, 500, 600, 700, 800, 900. Combined with literal 50 and 950, all eleven shades emit.

The flagship pattern (all colours, all shades, common variants) :

@source inline("{hover:,focus:,}bg-{red,blue,green,amber,purple,pink,zinc}-{50,{100..900..100},950}");

This single line generates 7 colours × 11 shades × 3 variant states = 231 rules. Hand-written safelist equivalent : 231 lines.

Excluding subsets with @source not inline :

@source not inline("bg-{red,green}-{700,800,900}");

When None of the Fixes Apply

Truly user-typed CSS (a CMS-managed Tailwind class string entered by non-developers) cannot be exhaustively safelisted because the input space is unbounded. Two options :

  1. Restrict the input field to a fixed allowlist of approved utility classes ; pre-safelist exactly that allowlist via Fix 3 or Fix 4.
  2. Convert the editor's affordance to design tokens (colour picker, size slider) that emit inline style (Fix 2).

NEVER attempt to safelist "all of Tailwind" by including every utility. The output CSS bloats to megabytes and the JIT performance benefit disappears.

Brace Expansion Grammar (v4 @source inline)

The full grammar :

Pattern Generates
{a,b,c} a, b, c
{a,,b} a, `` (empty), b
{a..z} a, b, c, ..., z (single-char range)
{1..10} 1, 2, 3, ..., 10 (numeric range, step 1)
{0..100..10} 0, 10, 20, ..., 100 (numeric range, step 10)
{a,b}{1,2} a1, a2, b1, b2 (Cartesian product)

Nested expansion :

@source inline("bg-red-{50,{100..900..100},950}");

{100..900..100} is a nested range expansion inside the outer comma list.

Verification Checklist

  1. Production build : npm run build. Inspect the output CSS for one of the previously-missing selectors :
    grep -c "bg-red-500" dist/assets/*.css
    
  2. Toggle the dynamic prop : every value in the set must produce a visible style change. Manually click through each option.
  3. Clean dev server : kill the dev server, wipe .next/ or node_modules/.vite, restart. Classes still apply.
  4. Safelist drift check : every entry in safelist or @source inline must correspond to a real prop value still in code. Audit periodically.

References

  • references/methods.md : full implementation of each fix with framework-specific patterns (React, Vue, Svelte, server templates)
  • references/examples.md : working code for every fix, including the issue 18136 SSR scenario
  • references/anti-patterns.md : ten wrong fixes that people try before they find the right one

Sources