How to Make a Safelist in Tailwind v4

February 1, 2025

I converted this site to Tailwind v4, and it was a bit painful. It certainly wasn’t as backwards compatible as I expected. I’m sure it’d be a fine upgrade for a more simple use case, however, I replace what you’d normally do with Javascript with plain CSS across my site. Needless to say, a lack of a feature for safelists was one of those painpoints.

A blog post from Tailwind originally stated that safelists were on their roadmap. It’s still unclear to me what their plans for the future are, however, the official docs for v4 now state “The corePlugins, safelist and separator options from the JavaScript-based config are not supported in v4.0.”.

The first place we should read through is the “Detecting classes in source files” page in the new docs. It’s a bit of a prerequisite to this article. I’m not going to reiterate it because I think the Tailwind docs explain it well.

As the docs seem to imply, if you make sure to use static classes rather than dynamic classes, you won’t need a safelist… but what if we need to support classes that are truly dynamic?

Solution

As long as you know what all of the classes you could possibly need are at build time, there is a way to take advantage of the fact that Tailwind detects your classes from your project files as plain text.

What if we put all of our dynamic classes into a plain text file? Kind of a crazy idea, but it should work based on how Tailwind detects classes.

How are we going to get them there? We can write a simple node script that generates a safelist text file for us. Node’s offical docs has good information on this.

If you’re using Typescript, there’s a section on that towards the end of the article.

If you’re using ES Modules:

generate-safelist.mjs
import * as fs from "fs";
import { classes } from "./classes.mjs";
fs.writeFile("safelist.txt", classes.join("\n"), (err) => {
if (err) {
console.error(err);
} else {
console.log("Safelist created");
}
});

If you’re using CommonJS:

generate-safelist.cjs
const fs = require("node:fs");
const { classes } = require("./classes.cjs");
fs.writeFile("safelist.txt", classes.join("\n"), (err) => {
if (err) {
console.error(err);
} else {
console.log("Safelist created");
}
});

Here’s where we’re dynamically generating our classes, but obviously this would be your own code with your own dynamic classes.

ES Module:

classes.mjs
export const classes = [...Array(100).keys()].map((i) => `p-${i}`);

CommonJS:

classes.cjs
const classes = [...Array(100).keys()].map((i) => `p-${i}`);
module.exports = { classes };

You will probably want to add this to your package.json scripts. Note that depending on the file extension of your generate-safelist file, you may need to include that as well.

package.json
"scripts": {
"...": "...",
"generate-safelist": "node generate-safelist"
},

You could totally make this a part of your dev & build scripts, and gitignore the file if you wanted. The example below is an Astro project, but your scripts would obviously match whatever framework you’re using.

package.json
"scripts": {
"dev": "npm run safelist && astro dev",
"start": "npm run safelist && astro dev",
"build": "npm run safelist && astro check && astro build",
"...": "...",
"generate-safelist": "node generate-safelist"
},

You may want to .gitignore the file.

.gitignore
...
safelist.txt

You’ll need to include your safelist file using @source, so that Tailwind knows to read the file and detect classes from it. You can read Tailwind’s docs on this. During my tests, even when the file wasn’t gitignored, I still needed to register the file with @source.

your-css.css
@source "safelist.txt";

You could write your classes to a Javsacript or Typescript file instead of a text file, if you wanted.

generate-safelist.js
const content = 'export const safelist = {\n' + classes.map((className, i) => ` ${i}: "${className}"`).join(",\n") + "\n};\n";
fs.writeFile("safelist.js", content, (err) => {
if (err) {
console.error(err);
} else {
console.log("Safelist created");
}
});

If you don’t gitignore this file, you shouldn’t need to use @source.

You could use your classes as shown below if you choose to take this approach. The following example is JSX, so implementation for your framework may vary.

example.jsx
import { safelist } from "safelist";
export default function Example() {
return (
<div className={`bg-red-500 ${safelist[15]}`}>
<p>This is an example.</p>
</div>
);
}

Typescript

If the file (or files) you are importing your dynamic classes from is (are) Typescript, you’ll need to use tsx instead of node.

You can easily do this with npx or you can install tsx into your project.

package.json
"scripts": {
"...": "...",
"generate-safelist": "npx tsx generate-safelist"
},

If you want to install tsx into your project, see below.

Terminal window
npm install -D tsx
package.json
"scripts": {
"...": "...",
"generate-safelist": "tsx generate-safelist"
},

You may also need to install @types/node.

Terminal window
npm install --save @types/node

If you want to type the err variable in our example, see below.

generate-safelist.ts
fs.writeFile("safelist.txt", classes.join("\n"), (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(err);
} else {
console.log("Safelist created");
}
});

Conclusion

In conclusion, Tailwind v4 doesn’t currently have a safelist feature. If we truly need a safelist, we can work around this by writing a node script to generate a safelist, and then be sure the file is read by Tailwind using @source.