D
P
0

CSS & Web Animation

Tailwind v4 `@source` Resurrects Classes You Already Deleted — Exclude the Seed/Import Folder

June 27, 2026·4 min read
Tailwind v4 `@source` Resurrects Classes You Already Deleted — Exclude the Seed/Import Folder

The symptom made me briefly question my own sanity. I was cleaning out unused utility classes in a client's custom theme — old classes that should have been dead once I reworked the components that used them. I deleted the class from the PHP templates, ran the Tailwind build, looked at the compiled CSS, and... the class was still there. I deleted it again, rebuilt again, still there. No error, no warning, nothing. The compiled output stayed full of classes I was certain I had removed.

The setup looked roughly like this. This is Tailwind v4, and in my CSS entry I had a very broad @source to make sure every template got scanned:

@import "tailwindcss";
 
@source "../..";

That @source "../.." points at the project root. The intent was good: don't let any template slip through. The problem is that the project root contained one folder that was never part of what ships to production.

Why this happens

The project had a seed-import folder — a pile of HTML files from a one-time content migration. That folder existed only for the initial import: a dump of the old markup so the content could be moved across, and after that it was never supposed to be touched again. It never gets deployed. But it still sat on disk, inside the project root, full of old markup that was still littered with legacy classes.

That is exactly where things broke. Tailwind's content scanner is greedy. It does not care whether a file actually renders on the site or just sits on disk. It reads every file that matches the @source glob, hunts for any string that looks like a class name, and generates CSS for all of them. The scanner does not evaluate your code and has no notion of what is "used" at runtime — to it, a string that matches the class pattern is indistinguishable from a class you actually use.

So the chain is clear now. I removed .btn-legacy (and friends) from the PHP templates that actually ship. But the exact same btn-legacy string was still alive inside the old HTML files in seed-import/. Because @source "../.." covered that folder too, Tailwind found it, treated it as "used," and regenerated the CSS for it. Every rebuild, the scanner rediscovered those classes from the seed folder and brought them back. I thought I was deleting classes; Tailwind thought it was finding them. We were both right, which is precisely what made it maddening.

What made this bug slippery is that there was no failure signal at all. The build succeeded. The CSS was valid. The classes genuinely "had a source" — it just happened to be a file I never opened and never thought about. As long as I only looked at the template folder, everything looked exactly the way it should if those classes were already gone.

The fix

Once I understood the scanner was reading the wrong folder, the solution was obvious: keep the non-shipping folder out of the scan. Tailwind v4 has an exclusion syntax using @source not:

@import "tailwindcss";
 
@source "../..";
@source not "../../seed-import";

Rebuild, and the dead classes are gone. Actually gone. The seed-import folder is now skipped by the scanner, so the only thing deciding which classes get generated is the templates that actually ship. Remove a class from a template, build, the class disappears — exactly what I expected from the start.

If you want to be stricter (and I lean this way), don't scan the project root at all. Narrow the include globs so they only point at the real template directories, and skip the exclusion entirely:

@import "tailwindcss";
 
@source "../templates";
@source "../parts";

I like this approach because it inverts the default. Instead of scanning everything and then carving out the dangerous bits one at a time, you only scan what ships. New folders that show up later — another seed dump, a vendor dump, build output — won't get scanned automatically unless you deliberately add them. To me that is a safer default posture.

Which one you pick depends on your project layout. @source not is nice when your templates are scattered across many places and it is easier to scan broadly and exclude the one problem folder. The narrowed globs are better when your templates already live in a few clear directories. Both solve the core problem: don't let the scanner read markup you will never ship.

The takeaway

Tailwind's content scan is greedy, and it treats every matching string as a used class. It has no idea which files are "live" and which are just artifacts — migration markup, seed dumps, vendor snippets, build output, it is all the same if it falls within @source reach. If you delete a class and it keeps reappearing in the compiled CSS, don't jump straight to suspecting caching or a build-tool bug. Ask first: where else is this string still alive on disk?

The rule of thumb I keep now: exclude generated, seed, vendor, and import folders from the Tailwind scan — either with @source not or by narrowing the include globs. Otherwise you will keep shipping dead utilities you thought you had deleted, and you will spend one strange afternoon staring at a diff that refuses to change.