Fix: Tailwind v4 Not Working — @theme, CSS-First Config, PostCSS vs Vite, and v3 Migration
Part of: React & Frontend Errors
Quick Answer
How to fix Tailwind CSS v4 errors — tailwind.config.js ignored, @import 'tailwindcss' not loading, @theme custom values not applied, content scanning misses files, Vite plugin setup, and v3 to v4 migration gotchas.
The Error
You upgrade to Tailwind v4 and classes stop applying:
<div class="bg-blue-500 p-4">Hello</div>
<!-- No styling. -->Or your tailwind.config.js is completely ignored:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: { brand: "#ff0080" },
},
},
};<div class="text-brand">Hello</div>
<!-- Still no color. -->Or the new @theme directive doesn’t work:
@import "tailwindcss";
@theme {
--color-brand: oklch(0.7 0.2 30);
}Or your PostCSS config errors out:
Error: It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin.
The PostCSS plugin has moved to a separate package.Why This Happens
Tailwind v4 is a ground-up rewrite. Three structural changes that break v3 setups:
- CSS-first config.
tailwind.config.jsis no longer the source of truth. Theme tokens live in CSS via@theme { ... }. The JS config still works for migration but is opt-in. - PostCSS plugin moved.
tailwindcssitself is no longer the PostCSS plugin. The plugin is now@tailwindcss/postcss. For Vite users, there’s a dedicated@tailwindcss/viteplugin that’s faster and recommended. @importreplaces@tailwinddirectives.@tailwind base; @tailwind components; @tailwind utilities;becomes a single@import "tailwindcss";.
The “classes don’t apply” issue is usually one of: the PostCSS plugin isn’t wired up, the new import directive is missing, or the content scanner doesn’t see your template files. The two other failure modes that bite hard are PostCSS plugin ordering (running @tailwindcss/postcss after autoprefixer or postcss-import instead of before) and content scanning silently missing files that live outside the project root, like a shared design-system package in a sibling directory.
Production Incident Lens: When a Tailwind Upgrade Breaks Production CSS
The Tailwind v4 production incident has a specific shape: the upgrade goes through CI green, the staging smoke test passes, you ship to production, and within 30 seconds users start reporting “the site looks broken.” The home page renders without spacing, the brand color is gone, dark mode is inverted, modals overflow the viewport. The blast radius is every page on the site at once, because CSS is global. Unlike a backend incident where one endpoint can degrade in isolation, a CSS regression is total — the entire visual surface goes down together.
Why does this slip past CI? Three reasons. First, snapshot tests usually run against unstyled DOM, so they pass even when styling is wrong. Second, visual regression tools (Percy, Chromatic) often get skipped on CSS-only PRs because reviewers assume “it’s just Tailwind, classes don’t change.” Third, the v3-to-v4 migration tool handles 90% of cases automatically — but the 10% it misses are exactly the custom plugins and safelist entries that hold the design system together.
When the page fires, your fastest rollback is at the edge: serve the previous build’s CSS bundle from your CDN while you diagnose. If you’re on Cloudflare Pages, Vercel, or Netlify, the previous deployment is one click away. Don’t try to hot-fix Tailwind during the incident — too many moving parts. Roll back, then reproduce locally with pnpm build && pnpm preview, diff the generated CSS file against the previous green build, and look for missing utility classes. A single missing bg-brand in the output usually traces to a @theme token that didn’t get re-declared after the upgrade tool flattened the JS config.
The right monitoring signal is CSS bundle size delta per deploy. A v4 upgrade that drops the bundle from 80KB to 12KB usually means content detection failed and most utility classes got tree-shaken out as unused. Wire this into your CI as a budget check: fail the build if the CSS bundle shrinks by more than 30% deploy-to-deploy without a corresponding code change. It catches the worst class of “looks broken on production” before users see it.
Fix 1: Update the PostCSS or Vite Plugin
For Vite projects (Astro, SvelteKit, React with Vite, etc.) — switch to the Vite plugin:
npm install -D tailwindcss @tailwindcss/vite// vite.config.ts
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Remove the old PostCSS config if it only existed for Tailwind. The Vite plugin runs Tailwind directly — no PostCSS round-trip.
For Next.js, Remix, or other PostCSS-driven setups:
npm install -D tailwindcss @tailwindcss/postcss// postcss.config.mjs (or postcss.config.js)
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};Remove tailwindcss from your PostCSS plugins — that’s v3. The plugin moved.
Pro Tip: The Vite plugin is dramatically faster than PostCSS for Tailwind. If your bundler supports it, prefer it.
Fix 2: Use @import "tailwindcss" Once
In your single global CSS file:
@import "tailwindcss";That’s it. No @tailwind base, @tailwind components, @tailwind utilities. The single import pulls in all of Tailwind.
For more control, you can import sub-layers individually:
@import "tailwindcss/preflight";
@import "tailwindcss/utilities";
/* Skipping the components layer */But the recommended pattern is the single import.
Common Mistake: Mixing v3 and v4 directives:
/* WRONG: */
@import "tailwindcss";
@tailwind utilities;
/* Correct: */
@import "tailwindcss";If you copy-pasted from a v3 tutorial, find and replace the old directives.
Fix 3: Define Theme Tokens With @theme
Custom colors, spacing, fonts — everything that used to live in theme.extend — goes in a @theme block:
@import "tailwindcss";
@theme {
--color-brand: oklch(0.7 0.2 30);
--color-brand-soft: oklch(0.85 0.1 30);
--font-display: "Geist", "system-ui", sans-serif;
--spacing-128: 32rem;
--breakpoint-3xl: 1920px;
}These become available as utility classes:
--color-brand→bg-brand,text-brand,border-brand, etc.--font-display→font-display--spacing-128→p-128,m-128,w-128--breakpoint-3xl→3xl:prefix
The naming convention (--color-*, --font-*, --spacing-*, --breakpoint-*) is how Tailwind knows what category each variable belongs to.
Note: Theme variables become real CSS custom properties on :root. You can read them in any CSS too: color: var(--color-brand).
Fix 4: Use the JS Config Compatibility Mode (Migration Path)
To keep your tailwind.config.js working during migration, opt in explicitly:
@import "tailwindcss";
@config "../../tailwind.config.js";The @config directive tells Tailwind to load and merge a v3-style config. This is the bridge while you migrate theme.extend entries into @theme.
Once you’ve moved everything to CSS, delete the config file and remove the @config line.
Common Mistake: Setting up @config and @theme for the same token. The CSS @theme wins. If your JS color isn’t appearing, check whether it’s also defined in @theme.
Fix 5: Content Detection Is Automatic But Has Limits
Tailwind v4 auto-detects template files in your project — no content array needed in most cases. It scans:
- Files in your project (excluding
node_modulesand.gitignored paths by default). - Common template extensions:
.html,.js,.jsx,.ts,.tsx,.vue,.svelte,.astro, etc.
If classes from a particular file aren’t appearing:
@import "tailwindcss";
@source "../node_modules/some-ui-library/dist/**/*.js";@source tells Tailwind to scan extra paths. Use it when:
- A third-party UI library ships pre-built JS with Tailwind classes you want to keep.
- You generate templates outside the default scanned paths.
To explicitly exclude a path:
@source not "./src/legacy/**";Pro Tip: If you migrated from v3 and had a long content: [...] array, replace each entry with @source "...". The behavior is similar.
Fix 6: Dark Mode Configuration
v4 default is prefers-color-scheme. To use a class-based toggle:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));Now toggling <html class="dark"> switches the theme. This pattern is more flexible than v3’s darkMode: 'class' — you can use any selector, like a data-theme="dark" attribute:
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));The @custom-variant directive replaces v3’s variant config. Use it for any selector-based variant you need.
Fix 7: Plugins and @plugin
Third-party Tailwind plugins still work but are loaded differently:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@plugin "./my-local-plugin.js";No more plugins: [require('@tailwindcss/typography')] in JS config (unless you’re using @config compatibility).
For first-party plugins, install and reference:
npm install -D @tailwindcss/typography @tailwindcss/formsCustom plugins still use plugin() from tailwindcss/plugin:
// my-local-plugin.js
import plugin from "tailwindcss/plugin";
export default plugin(({ addUtilities }) => {
addUtilities({
".text-shadow": { textShadow: "0 1px 2px rgba(0,0,0,0.1)" },
});
});Fix 8: Migrate From v3 With the Upgrade Tool
The official upgrade tool handles most v3 → v4 conversions:
npx @tailwindcss/upgradeIt:
- Replaces
@tailwinddirectives with@import "tailwindcss". - Converts simple
tailwind.config.jsthemes into@themeblocks in your CSS. - Updates PostCSS config to use
@tailwindcss/postcss. - Removes deprecated utility classes (
flex-grow-0→grow-0, etc.). - Adds
@configfor complex configs it couldn’t auto-migrate.
Run it on a clean git tree and review the diff carefully. Known cases the tool can’t auto-fix:
- Custom plugins that touch internal APIs (the plugin API was simplified — check each plugin).
- Theme tokens that depended on JS functions (e.g. dynamically generated colors).
safelistentries — review and convert to explicit class usage or@sourceincludes.
Common Mistake: Running the upgrade on a project with a half-migrated state. Either commit your v3 state first and run the tool, or finish manual migration before running it.
Still Not Working?
A few less-obvious failures:
oklch()color not rendering on old browsers. v4 uses modern color spaces by default. Browsers older than ~2023 don’t supportoklch. Polyfill with@supportsor use the fallback the Tailwind upgrade tool provides.- Build size is huge after upgrade. v4 ships more by default. Inspect the generated CSS or use the Tailwind CLI’s verbose flag (check
tailwindcss --helpfor your version) to see which files are scanned. Use@source not "..."to exclude generated/test files. - Arbitrary values like
text-[18px]not working. Should still work — confirm the import directive is correct and the file is being scanned. Check for typos in the bracket syntax. @layer base { ... }styles not applying. v4 still supports@layer, but the layer cascade order changed.@layer basestyles come before component utilities; ensure you’re not accidentally overriding them later.- TypeScript types missing for
tailwindcss/plugin. Install@types/tailwindcssif your editor complains, though v4 ships its own types in most setups. - Hot reload not picking up CSS changes. The Vite plugin should handle this. For PostCSS, ensure
@tailwindcss/postcssis fresh enough to support HMR. @applyslower than v3. v4 changed@applyinternals. For hot paths, consider inlining the utility classes directly rather than wrapping in@apply.- Astro/SvelteKit project: classes work in
.astrofiles but not in JS-generated components. Add the JS path with@sourceif it’s outside the auto-scan defaults.
CSS Bundle Shrinks Drastically After Upgrade
If the generated CSS file is 10x smaller than v3 produced, content detection isn’t picking up your templates. Run the build with verbose logging and check the list of scanned files. Most often the culprit is a workspace package (under packages/ui in a monorepo) that v3 scanned because of an explicit content entry but v4 ignores because it’s outside the default project boundary. Add @source "../packages/ui/src/**/*.{ts,tsx,vue,svelte,astro}"; to the entry CSS file.
Dark Mode Inverts On First Paint Then Snaps to Correct Mode
You’re seeing a FOUC because the @custom-variant dark directive resolves after the HTML loads. Set the class="dark" (or data-theme="dark") on <html> from an inline script at the top of <head> before any CSS loads — read the stored preference from localStorage and apply it synchronously. This is unchanged from v3 but easy to forget when redoing the dark-mode wiring during the v4 migration.
Monorepo Package Ships Its Own Tailwind and Conflicts
A shared UI package built with its own Tailwind copy injects styles at runtime that fight the consumer app’s styles. The v4-correct pattern is to ship the UI package as uncompiled templates (the consumer’s Tailwind scans them) or as already-resolved CSS that consumers import once. Don’t ship both. The hybrid path explodes specificity and leaves you debugging which bg-blue-500 wins.
For related CSS and Tailwind issues, see Tailwind classes not applying, CSS tailwind not applying, Vite failed to resolve import, and CSS variable not working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Tailwind CSS Classes Not Applying — Styles Missing in Production or Development
How to fix Tailwind CSS not applying styles — content config paths, JIT mode, dynamic class names, PostCSS setup, CDN vs build tool, and purging issues.
Fix: Mantine Not Working — Styles Not Loading, Theme Not Applying, or Components Broken After Upgrade
How to fix Mantine UI issues — MantineProvider setup, PostCSS configuration, theme customization, dark mode, form validation with useForm, and Next.js App Router integration.
Fix: View Transitions API Not Working — No Animation Between Pages, Cross-Document Transitions Failing, or Fallback Missing
How to fix View Transitions API issues — same-document transitions, cross-document MPA transitions, view-transition-name CSS, Next.js and Astro integration, custom animations, and browser support.
Fix: Panda CSS Not Working — Styles Not Applying, Tokens Not Resolving, or Build Errors
How to fix Panda CSS issues — PostCSS setup, panda.config.ts token system, recipe and pattern definitions, conditional styles, responsive design, and integration with Next.js and Vite.