Skip to content

Fix: Tailwind v4 Not Working — @theme, CSS-First Config, PostCSS vs Vite, and v3 Migration

FixDevs ·

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.js is 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. tailwindcss itself is no longer the PostCSS plugin. The plugin is now @tailwindcss/postcss. For Vite users, there’s a dedicated @tailwindcss/vite plugin that’s faster and recommended.
  • @import replaces @tailwind directives. @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.

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-brandbg-brand, text-brand, border-brand, etc.
  • --font-displayfont-display
  • --spacing-128p-128, m-128, w-128
  • --breakpoint-3xl3xl: 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_modules and .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/forms

Custom 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/upgrade

It:

  • Replaces @tailwind directives with @import "tailwindcss".
  • Converts simple tailwind.config.js themes into @theme blocks in your CSS.
  • Updates PostCSS config to use @tailwindcss/postcss.
  • Removes deprecated utility classes (flex-grow-0grow-0, etc.).
  • Adds @config for 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).
  • safelist entries — review and convert to explicit class usage or @source includes.

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 support oklch. Polyfill with @supports or 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 --help for 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 base styles come before component utilities; ensure you’re not accidentally overriding them later.
  • TypeScript types missing for tailwindcss/plugin. Install @types/tailwindcss if 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/postcss is fresh enough to support HMR.
  • @apply slower than v3. v4 changed @apply internals. For hot paths, consider inlining the utility classes directly rather than wrapping in @apply.
  • Astro/SvelteKit project: classes work in .astro files but not in JS-generated components. Add the JS path with @source if it’s outside the auto-scan defaults.

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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles