Tailwind Build Failures : Why Your Classes Don't Render
The Tailwind compiler emits ONLY the utility classes it can DETECT in your source files. Every "Tailwind is broken" report is one of :
- The compiler did not see the file containing the class.
- The compiler saw the file but could not parse the class as a complete unbroken string.
- The class was detected, compiled, but a stale build cache served yesterday's CSS to the browser.
- A version-specific regression in v4 or Turbopack swallowed certain class patterns silently.
This skill is the diagnostic flow for all four. Always go top to bottom : root-cause-first.
Companion skills :
tailwind-impl-config-v3: thecontentarray surfacetailwind-impl-config-v4: the@sourcedirective surfacetailwind-impl-build-vite: Vite-specific cache behaviourtailwind-impl-build-postcss-cli: PostCSS / CLI flowtailwind-errors-runtime: when classes ARE in CSS but not appliedtailwind-errors-v4-migration: v3-to-v4 specific breakage
Quick Reference : 60-Second Diagnostic
1. Inspect compiled CSS (open the file the browser loads).
- File is EMPTY -> scanner found nothing
- File has SOME classes, missing X -> X was not detected
- File looks right but browser shows old styles -> cache stale
- File is missing a recently added class -> watcher missed it
2. Search compiled CSS for the missing class :
grep "p-4" dist/output.css
3. If absent : the SCANNER missed it. Go to "Scanner Diagnosis".
If present : the BUILD is stale OR another rule wins. Go to
"Cache Diagnosis" + tailwind-errors-runtime.
Decision Tree : Scanner Missed the Class
Is the class an UNBROKEN STRING in source ?
├── NO (string interpolation: `bg-${color}-500`)
│ -> FIX: replace with static lookup (see "Static-Class Discipline")
│
└── YES
├── v3 : Is the file inside `content` globs ?
│ ├── NO -> add the glob OR widen existing one
│ └── YES -> check next branch
│
└── v4 : Is the file inside auto-detect scope OR explicit @source ?
├── NO (file in node_modules, monorepo sibling, etc.)
│ -> Add @source "../path/to/file"
└── YES
├── Turbopack ? -> see "Turbopack Workaround"
├── Astro v4.0.8 ? -> pin to v4.0.7
└── Otherwise -> restart watcher (new-directory case)
Decision Tree : Stale Cache
Did the class appear in CSS yesterday but not today after a code change ?
Vite project ?
-> rm -rf node_modules/.vite ; npm run dev
Next.js project ?
-> rm -rf .next ; npm run dev
Webpack project ?
-> Disable persistent cache OR rm -rf node_modules/.cache
Astro project ?
-> rm -rf .astro dist node_modules/.vite ; npm run dev
CLI watch mode ?
-> Ctrl-C, restart npx (@tailwindcss/cli OR tailwindcss)
CRITICAL : Static-Class Discipline
90% of "Tailwind is broken" reports trace to ONE pattern.
Wrong (works in browser console, fails in build)
function Button({ color, size }) {
return <button className={`bg-${color}-600 text-${size}`}>...</button>;
}
At compile time, Tailwind sees the strings bg-, -600, text- but
NEVER the complete bg-blue-600. The class never enters compiled CSS.
Right (complete class strings)
function Button({ color, size }: { color: "blue" | "red"; size: "sm" | "lg" }) {
const colorClasses = {
blue: "bg-blue-600 hover:bg-blue-700",
red: "bg-red-600 hover:bg-red-700",
};
const sizeClasses = {
sm: "text-sm",
lg: "text-lg",
};
return <button className={`${colorClasses[color]} ${sizeClasses[size]}`}>...</button>;
}
Every class appears as a complete unbroken string somewhere in source. The scanner finds them.
Right (safelist when dynamic is unavoidable)
v3 :
// tailwind.config.js
module.exports = {
safelist: [
"bg-red-500",
{
pattern: /bg-(red|green|blue)-(100|200|300|400|500)/,
variants: ["hover", "focus"],
},
],
};
v4 :
@import "tailwindcss";
@source inline("{hover:,focus:,}bg-{red,green,blue}-{100,200,300,400,500}");
The brace expansion in @source inline is the v4 equivalent of
v3's safelist pattern.
v3 Content Array : The Survival Kit
Minimum
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{html,js,jsx,ts,tsx,vue,svelte}"],
};
Common mistakes (and fixes)
| Symptom | Cause | Fix |
|---|---|---|
New app/admin/ directory unstyled |
Pre-existing glob ./src/** does not include app/ |
Add "./app/**/*.{js,ts,jsx,tsx}" |
| Classes from a CMS-rendered HTML missing | HTML files not in content | Add "./public/**/*.html" or wherever HTML lives |
| Classes in MDX files missing | .mdx not in extension list |
Append mdx : "./content/**/*.{md,mdx}" |
Tailwind picks up node_modules (build slow) |
Glob too broad (./**/*) |
Scope to specific dirs |
Classes from imported @my-org/ui package missing |
The package's source not in content | Add "./node_modules/@my-org/ui/**/*.{js,jsx,ts,tsx}" |
Safelist for truly-dynamic strings
safelist: [
"bg-red-500", "bg-green-500", "bg-blue-500",
{ pattern: /^col-span-\d+$/, variants: ["sm", "md", "lg"] },
],
Blocklist to suppress accidental classes
blocklist: ["container"], // strip 'container' if you defined your own
v4 @source Directive : The Survival Kit
Default auto-detection scope
- All files in current working directory, recursively.
- IGNORED : files matched by
.gitignore,node_modules, binary files, CSS files, package-manager lock files.
Adding a monorepo sibling
@import "tailwindcss";
@source "../packages/ui/src";
@source "../packages/marketing/src";
Adding a node_modules package
@import "tailwindcss";
@source "../../node_modules/@my-org/ui";
Setting a different base path
@import "tailwindcss" source("../src");
Excluding a path
@source not "../src/legacy";
Disabling auto-detection entirely
@import "tailwindcss" source(none);
@source "../src/components";
@source "../src/pages";
Use this for monorepos where you want EXPLICIT control.
Inline safelist with brace expansion
@source inline("underline");
@source inline("{hover:,focus:,}underline");
@source inline("{hover:,}bg-red-{50,{100..900..100},950}");
The {n..m..step} range is a v4 feature. Pre-generates the entire
palette for dynamic class needs.
CRITICAL : Stale Build Cache
Tailwind itself is fast and deterministic. The host bundler's cache is what goes stale.
Vite + Tailwind v4
The Vite dependency-cache lives in node_modules/.vite. When you
edit tailwind.config.js or app.css, Vite sometimes serves cached
PostCSS output. Symptom : you delete bg-red-500 from source, it
still appears in the browser.
rm -rf node_modules/.vite
npm run dev
Next.js (App Router + Pages Router)
Next.js caches PostCSS output in .next/. After upgrading Tailwind
or switching v3-v4, force a fresh build :
rm -rf .next
npm run dev
Astro
Astro layers its own .astro cache on top of Vite's .vite cache :
rm -rf .astro dist node_modules/.vite
npm run dev
Standalone CLI watch
Kill the watcher (Ctrl-C) and restart. The watcher's chokidar (v3) or Oxide (v4) caches file lists at startup ; new-directory additions need a restart.
Known Version-Specific Regressions
Turbopack arbitrary-value miss (Next.js 16.x + Tailwind v4.2.1)
Per issue 19825 : aspect-[12/5], z-[100], and similar arbitrary-value
classes are emitted in markup but NEVER reach compiled CSS when using
Next.js 16.1.6 with --turbo. Affects files that import certain
third-party libraries (e.g. Swiper.js).
Workarounds :
# Option 1 : disable Turbopack (use Webpack)
next dev # no --turbo
// Option 2 : inline style instead of arbitrary value
<div style={{ aspectRatio: "12/5" }}>
<div style={{ zIndex: 100 }}>
// Option 3 : add the arbitrary class to @source inline so it
// is always generated regardless of detection
@source inline("aspect-[12/5] z-[100]");
Astro v4.0.8 regression (issue 16733)
Per issue 16733 : Tailwind v4.0.8 with @tailwindcss/vite v4.0.8 and
Astro v5.3.0 silently drops styles from imported component packages.
Workaround : pin to v4.0.7 until upstream resolves.
{
"dependencies": {
"tailwindcss": "4.0.7",
"@tailwindcss/vite": "4.0.7"
}
}
Lock both packages in package-lock.json / yarn.lock to prevent
auto-bump.
CRITICAL : v3-v4 Directive Mix-Up
| Project | Entry CSS line | Result |
|---|---|---|
| v3 | @import "tailwindcss"; |
Empty CSS, browser fetches URL |
| v3 | @tailwind base; @tailwind components; @tailwind utilities; |
Works |
| v4 | @tailwind base; ... |
Deprecation warnings + partial output |
| v4 | @import "tailwindcss"; |
Works |
Detection : if the compiled CSS is <2 KB, the directive line is
probably wrong.
CRITICAL : Watcher Misses New Directory
# v3
mkdir src/admin
# Watcher does NOT pick up files in src/admin if content glob is
# ./src/components/**/* rather than ./src/**/*
# v4
mkdir packages/widget
# Auto-detect picks it up if it's in cwd ; if it's a sibling outside
# cwd, ADD @source "../widget".
Fix : restart watcher AND widen content/@source scope.
What This Skill Does NOT Cover
- Classes ARE in CSS but browser shows wrong styles -> see
tailwind-errors-runtime(specificity, cascade, scope). - v3-to-v4 migration breakage -> see
tailwind-errors-v4-migration. - Forms / plugin classes missing -> see
tailwind-impl-plugins-official§ "Verifying a Plugin Loaded". cn/twMergereturns unexpected output -> seetailwind-impl-tailwind-merge.- HMR not updating React state (not a Tailwind issue) -> see the bundler's docs.
References
references/methods.md: every config option, every directive, every cache-invalidation command.references/examples.md: full diagnostic walk-throughs for the classic failure modes.references/anti-patterns.md: the traps catalogued by symptom.
Official Source Links
- v4 source detection : https://tailwindcss.com/docs/detecting-classes-in-source-files
- v3 content : https://v3.tailwindcss.com/docs/content-configuration
- Turbopack issue 19825 : https://github.com/tailwindlabs/tailwindcss/issues/19825
- Astro v4.0.8 issue 16733 : https://github.com/tailwindlabs/tailwindcss/issues/16733