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:
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:
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:
export const classes = [...Array(100).keys()].map((i) => `p-${i}`);
CommonJS:
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.
"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.
"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.
...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
.
@source "safelist.txt";
You could write your classes to a Javsacript or Typescript file instead of a text file, if you wanted.
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.
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.
"scripts": { "...": "...", "generate-safelist": "npx tsx generate-safelist"},
If you want to install tsx
into your project, see below.
npm install -D tsx
"scripts": { "...": "...", "generate-safelist": "tsx generate-safelist"},
You may also need to install @types/node
.
npm install --save @types/node
If you want to type the err
variable in our example, see below.
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
.