LEARN · DEBUGGING GUIDE

Tailwind CSS Purge Removing Used Classes: How to Debug

Tailwind's purge (now content) removes classes it thinks are unused. Most of the time it's because classes are built dynamically or live outside the configured content paths.

IntermediateBuild tools7 min read

What this usually means

Tailwind uses static analysis (regular expressions) to find class names in your source files. It does not execute your code, so any class name built dynamically—via concatenation, template literals, or conditional logic—will not be detected. The purge step then removes those "unused" classes from the final CSS. The same happens if your template files are not covered by the `content` configuration paths.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run `npx tailwindcss -i input.css -o output.css --content './src/**/*.html'` to manually inspect the generated CSS
  • 2Check the `content` array in `tailwind.config.js` – ensure it includes ALL file extensions and directories where classes appear
  • 3Search your codebase for dynamic class building patterns like `className={` or `class={` and list them
  • 4Add a test class (e.g., `bg-purple-500`) directly in a template file covered by content to confirm purge works on static classes
  • 5Temporarily set `purge: false` (or `safelist: ['*']`) to confirm the classes exist when purge is disabled
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • search`tailwind.config.js` – the `content` (or `purge`) array for missing paths or glob patterns
  • searchAll `.jsx`, `.tsx`, `.vue`, `.html`, `.php`, `.blade.php`, `.erb` files – any template where classes appear
  • searchGit blame on `tailwind.config.js` to see if content paths were recently changed
  • searchBuild logs – look for warnings like "The `purge` option is deprecated" or missing file counts
  • searchGenerated CSS file (e.g., `dist/output.css`) – search for a known class to see if it was emitted
  • searchCI/CD pipeline configuration – separate build steps may use different config files
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningDynamic class construction: `className={`btn-${variant}`}` – Tailwind cannot parse the variable
  • warningMissing file extensions in `content` array: e.g., `.tsx` omitted but components written in TypeScript
  • warningContent paths pointing to wrong directory: `'./src/**/*.html'` but components live in `./src/components/`
  • warningUsing `purge` option instead of `content` in Tailwind v3 (deprecated but still works – but may cause confusion)
  • warningClasses only used in string literals inside JavaScript event handlers or data attributes
  • warningThird-party library styles not included because they live in `node_modules` not covered by content
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAdd missing directories to `content`: `content: ['./src/**/*.{html,js,jsx,ts,tsx,vue}']`
  • buildRefactor dynamic classes to use complete class strings: replace `btn-${variant}` with map of all variants
  • buildUse `safelist` in config: `safelist: ['bg-red-500', 'text-center']` or a pattern `safelist: [{ pattern: /^bg-/ }]`
  • buildFor truly dynamic classes, add the full list to `safelist` or use a class generation library like `clsx` with explicit strings
  • buildIf using Tailwind v2, upgrade to v3 and migrate `purge` to `content`
  • buildFor classes in `node_modules`, add the library's dist folder to content: `'./node_modules/library-name/dist/**/*.js'`
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun `npx tailwindcss -i input.css -o output.css --content './src/**/*.html'` and grep for the missing class in output.css
  • verifiedDeploy a staging build and visually inspect the element with browser dev tools to confirm styles are present
  • verifiedAdd a safelist entry for the missing class, rebuild, and confirm the class now appears in the CSS
  • verifiedUse the `--verbose` flag during build: `npx tailwindcss -i input.css -o output.css --verbose` and check the list of discovered files
  • verifiedWrite a unit test that checks the generated CSS for expected classes using a snapshot or string search
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningAdding `purge: false` as a permanent fix – it disables tree-shaking and bloats your CSS
  • warningUsing `safelist: ['*']` – keeps all classes, defeats the purpose of purge
  • warningAssuming Tailwind can parse runtime-generated class strings – it cannot; use safelist or static strings
  • warningIgnoring whitespace or special characters in class names like `hover:bg-gray-100` – these are fine, but dynamic parts break
  • warningEditing `tailwind.config.js` without restarting the dev server – changes only apply on rebuild
  • warningNot checking the actual content paths for typos: `./src/**/*.html` vs `./src/**/*.htm`
( 07 )War story

Production CSS Missing Grid Classes After Migration to Tailwind v3

Frontend DeveloperNext.js 12, Tailwind CSS v3, PostCSS, Vercel

Timeline

  1. 09:00Deploy to production after upgrading Tailwind from v2 to v3
  2. 09:15User reports that dashboard grid layout is broken on mobile
  3. 09:20Inspect production CSS – `grid-cols-2`, `grid-cols-3` missing
  4. 09:25Check local dev – classes present; purge is the culprit
  5. 09:30Review `tailwind.config.js` – still using `purge` instead of `content`
  6. 09:35Change `purge` to `content`, add `.tsx` extension
  7. 09:40Rebuild and redeploy – grid classes now present
  8. 09:45Monitor – no further issues

We had just migrated from Tailwind v2 to v3. The migration guide said to rename `purge` to `content` in the config, but I skimmed it and left the old `purge` key. The build still ran because Tailwind v3 deprecated `purge` but still accepted it. However, the glob patterns were slightly different: `purge` expected an array of paths, while `content` expects the same. My config had `purge: ['./pages/**/*.js', './components/**/*.js']` – all good, but I was missing `.tsx` files because we had slowly migrated components to TypeScript.

The dashboard grid used `grid-cols-2` and `grid-cols-3` in a few `.tsx` components. Since those file extensions weren't in the config, Tailwind never scanned them. The classes were purged. I noticed the issue because a senior dev pointed out the missing grid on mobile. I started by checking the production CSS – it was only 12 KB, far smaller than the expected 80 KB. That was the first clue.

I compared the output of `npx tailwindcss -i input.css -o output.css --content './pages/**/*.js'` vs `--content './pages/**/*.tsx'`. The second produced the missing classes. I updated the config to: `content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}']`, changed `purge` to `content`, and redeployed. The grid came back. The lesson: always double-check file extensions in content paths, and never assume the migration is complete without verifying the build output.

Root cause

Content paths did not include `.tsx` files, so classes in TypeScript components were not scanned and were purged.

The fix

Updated `tailwind.config.js` to use `content` with explicit glob patterns covering all relevant file extensions (`.js`, `.ts`, `.jsx`, `.tsx`).

The lesson

Always verify that the content configuration covers all directories and file types where Tailwind classes are used, especially after migrations or adding new file types.

( 08 )How Tailwind's Purge (Content) Actually Works

Tailwind uses a library called `PurgeCSS` under the hood. It scans your files for strings that match Tailwind's class naming conventions. It does NOT execute JavaScript or resolve dynamic expressions. The scanning is purely regex-based.

The `content` array in `tailwind.config.js` tells Tailwind which files to scan. Each entry is a glob pattern. Tailwind collects all matched files, reads their contents, extracts all strings that look like Tailwind classes (e.g., `bg-red-500`, `md:flex`), and keeps only those classes in the final CSS. Any class not found in any scanned file is removed.

This means if you have a class like `text-${size}` where `size` is a variable, Tailwind sees `text-${size}` as a string – it does not evaluate it. Since that exact string is not a real class (there's no `text-{size}` class), it gets purged. The actual runtime values like `text-sm` or `text-lg` are never seen by Tailwind.

( 09 )Dynamic Class Construction Patterns That Break Purge

Common unsafe patterns include template literals: `<div className={`bg-${color}-500`}>`, string concatenation: `'bg-' + color + '-500'`, and conditional classes: `<div className={condition ? 'bg-red-500' : 'bg-green-500'}>` – actually this is safe because both strings are full class names.

The unsafe part is when part of the class name is computed. For example, a function that returns a class name: `getColorClass()` returning `'bg-red-500'` is fine because the full string appears in the source. But if the function builds the class from parts, the full string may never appear as a literal.

To fix, you can list all possible class names in a map: `const colorClasses = { red: 'bg-red-500', green: 'bg-green-500' }` and then use `colorClasses[color]`. Or use Tailwind's `safelist` option with a pattern like `safelist: [{ pattern: /^bg-/ }]` to keep all `bg-*` classes.

( 10 )Using Tailwind's Safelist Effectively

The `safelist` option in `tailwind.config.js` is the official way to prevent specific classes from being purged. It accepts an array of class names (exact strings) or objects with a `pattern` key and optional `variants`.

Example: `safelist: ['bg-red-500', { pattern: /^text-/, variants: ['hover', 'focus'] }]` keeps the exact class `bg-red-500` and all `text-*` classes with hover and focus variants.

Be careful with over-broad patterns like `pattern: /.*/` – it keeps all classes, which defeats the purpose of purge. Instead, use specific patterns or, better, refactor your code to use static class names where possible.

( 11 )Debugging Purge with Verbose Output

Tailwind's CLI has a `--verbose` flag that prints detailed information about which files were scanned and how many classes were extracted. Run: `npx tailwindcss -i input.css -o output.css --verbose`.

The verbose output shows each file it scans and the number of classes found. If a file you expect to be scanned is missing from the list, you know the content path is wrong. Also, it shows the total number of classes after purging – a sharp drop from development suggests purge is too aggressive.

( 12 )Migrating from Tailwind v2 to v3: Purge to Content

In Tailwind v2, the `purge` option was used. In v3, it's renamed to `content`. While `purge` still works in v3 (with a deprecation warning), it's best to migrate to `content` because future versions may remove it.

The key difference is that `purge` allowed an object with `enabled`, `content`, and `options`, while `content` is a simple array of globs. If you had `purge.enabled: true` and `purge.content: [...]`, you should move the array to `content` directly.

Also, v3 changed the default scanning to include all files in the `content` paths, whereas v2 required you to specify file types. So a minimal config in v3 is `content: ['./src/**/*.{html,js}']`.

Frequently asked questions

Why does Tailwind's purge remove classes that I clearly use in my HTML?

The most common reason is that the HTML file is not covered by the `content` paths in your config. Check that the glob patterns include the directory and file extension of that HTML file. Also, if the class name is built dynamically (e.g., via JavaScript concatenation), Tailwind cannot see the final class name.

Can I use regular expressions in safelist to keep all classes of a certain type?

Yes, you can use objects with a `pattern` key. For example, `safelist: [{ pattern: /^bg-/ }]` keeps all classes that start with `bg-`. You can also add `variants` to keep variant combinations like `hover:bg-`.

Does Tailwind scan files in `node_modules` by default?

No, Tailwind does not scan `node_modules` unless you explicitly add the path to `content`. If you are using a UI library that provides Tailwind classes, you need to add its dist folder, e.g., `'./node_modules/@library/dist/**/*.js'`.

How do I check which files Tailwind is scanning during build?

Use the `--verbose` flag: `npx tailwindcss -i input.css -o output.css --verbose`. It will list every file it scans. If you don't see your file, adjust the content paths.

I have a class like `md:grid-cols-3` that works in dev but not in production. Why?

If the class is built statically (e.g., written directly in the template), it should be kept. The issue might be that the file containing it is not in the content paths for the production build. Also, check if you have multiple Tailwind config files – one for dev and one for production – and ensure both include the file.