# Force-Include Classes with @source inline(), Not safelist

**SHOULD** · **ID:** `performance-purge-optimization` · **Category:** performance
**Source:** [Tailwind](https://tailwindcss.com/docs)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/performance-purge-optimization

> SHOULD: Keep content paths specific; minimize safelist to only CMS-driven classes

The safelist config option is gone in v4 — force-generate classes from CSS, and keep the set tiny

v4 removed `safelist` along with the rest of the JS config. The replacement lives in CSS: `@source inline("bg-red-500")` tells Tailwind to emit that utility even though it appears nowhere in the scanned source — the case that actually needs it being class names that arrive at runtime from a CMS, an API, or a database. Brace expansion keeps it from becoming a wall of strings: `@source inline("{hover:,}bg-red-{50,{100..900..100},950}")` generates the whole red scale plus every hover: variant of it. That is also the trap. That one line is ~22 utilities; do it for six colors and you have quietly force-shipped every one of them whether the CMS uses them or not, which is exactly the bloat the scanner exists to prevent. Inline the smallest set the data can actually produce, not the design system's full range. And note what this is NOT for: a class you build with string interpolation in your own code (`bg-${color}-500`) is not a safelist problem, it is a bug — write the complete class names out (see performance-dynamic-classes) instead of force-generating around it.

## Bad — do not do this

`performance-purge-optimization-bad`

```tsx
export function PurgeOptimizationBad() {
  return (
    <div className="w-full max-w-md space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="text-sm font-medium mb-1">tailwind.config.js</h4>
        <p className="text-xs text-muted-foreground mb-3">Tailwind v4 — <code>safelist</code> no longer exists</p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-foreground">{`export default {
  content: ['./**/*.{js,ts,jsx,tsx}'],
  safelist: [
    'bg-red-500', 'bg-blue-500',
    'text-red-500', 'text-blue-500',
    // ...100 more, "just in case"
  ],
}`}</pre>
        </div>
      </div>
      <div className="space-y-2">
        <div className="flex items-start gap-2">
          <span className="mt-1.5 size-2 shrink-0 rounded-full bg-error" />
          <span className="text-sm">
            <code>safelist</code> is silently ignored — the CMS colors never ship
          </span>
        </div>
        <div className="flex items-start gap-2">
          <span className="mt-1.5 size-2 shrink-0 rounded-full bg-error" />
          <span className="text-sm">
            The bug surfaces in production, as unstyled badges
          </span>
        </div>
      </div>
      <p className="text-xs text-destructive">
        A dead config that reads as a fix. Worse than no safelist at all, because it looks like the problem was handled
      </p>
    </div>
  );
}
```

## Good — do this

`performance-purge-optimization-good`

```tsx
export function PurgeOptimizationGood() {
  return (
    <div className="w-full max-w-md space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="text-sm font-medium mb-1">app.css</h4>
        <p className="text-xs text-muted-foreground mb-3">Tailwind v4 — force-include from CSS</p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-foreground">{`@import "tailwindcss";

/* Badge colors come from the CMS at runtime,
   so they appear in no source file.
   Only the 4 the CMS can actually emit: */
@source inline("bg-{red,amber,emerald,sky}-100");

/* Skip a huge generated fixture directory */
@source not "./src/__fixtures__";`}</pre>
        </div>
      </div>
      <div className="space-y-2">
        <div className="flex items-start gap-2">
          <span className="mt-1.5 size-2 shrink-0 rounded-full bg-success" />
          <span className="text-sm">
            Brace expansion keeps it to one line, not four
          </span>
        </div>
        <div className="flex items-start gap-2">
          <span className="mt-1.5 size-2 shrink-0 rounded-full bg-success" />
          <span className="text-sm">
            Scoped to the values the data can produce — not the whole palette
          </span>
        </div>
      </div>
      <p className="text-xs text-success">
        Expansion cuts both ways:{' '}
        <code>{'@source inline("{hover:,}bg-red-{50,{100..900..100},950}")'}</code> is legal and generates ~22
        utilities from one line. Inline the smallest set your data can actually produce
      </p>
    </div>
  );
}
```

## References

- [Tailwind — Safelisting classes (@source inline)](https://tailwindcss.com/docs/detecting-classes-in-source-files)
- [Tailwind — Functions and directives (@source)](https://tailwindcss.com/docs/functions-and-directives)
- [Tailwind — v4 upgrade guide](https://tailwindcss.com/docs/upgrade-guide)
