Skip to main content
ASoc
Tutorial

Tailwind CSS v4 Migration: What Actually Changed, and How to Update

Tailwind v4 moved configuration into CSS and dropped tailwind.config.js. Here is what breaks, what the @theme block replaces, and the order to migrate in.

The ASoc Team10 min read

Tailwind CSS v4 replaces tailwind.config.js with a @theme block in your stylesheet, swaps the PostCSS plugin for @tailwindcss/postcss, and renames a handful of utilities. Most projects migrate in an afternoon. The parts that take longer are custom plugins, arbitrary-value edge cases, and any tooling that read your config file directly.

Here is what actually changes, in the order we would fix it.

The short version

Areav3v4
Configtailwind.config.js@theme block in CSS
Import@tailwind base/components/utilities@import "tailwindcss"
PostCSS plugintailwindcss@tailwindcss/postcss
Dark modedarkMode: "class"@custom-variant dark (...)
Content pathscontent: [...]Automatic detection
Browser floorIE-era fallbacks availableSafari 16.4+, Chrome 111+, Firefox 128+

That last row is the one to check before anything else. Tailwind v4 is built on native cascade layers, @property, and color-mix(). If you must support browsers older than roughly early 2023, stay on v3 — no amount of migration work changes that.

Step 1: Update the toolchain

npm install tailwindcss@latest @tailwindcss/postcss
npm uninstall autoprefixer postcss-import

Both autoprefixer and postcss-import are now built in. Leaving them installed usually still works but adds redundant passes.

// postcss.config.mjs
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;

Tailwind ships an automated codemod that handles a good share of the mechanical work:

npx @tailwindcss/upgrade

Run it on a clean branch. It rewrites your CSS imports, converts most of the config, and flags what it could not translate. Treat its output as a first draft, not a finished migration.

Step 2: Replace the directives

/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* v4 */
@import "tailwindcss";

One line. If you were importing partials with @import before the Tailwind directives, they now need @reference or plain @import handled by the bundler — v4's import handling is stricter about ordering.

Step 3: Move your theme into CSS

This is the substantive change. Everything that lived under theme.extend becomes a CSS custom property in a @theme block, and the naming is mechanical:

// v3 — tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: "#465fff",
          500: "#465fff",
          600: "#3641f5",
        },
      },
      fontFamily: {
        sans: ["Outfit", "sans-serif"],
      },
      spacing: {
        18: "4.5rem",
      },
    },
  },
};
/* v4 — globals.css */
@import "tailwindcss";

@theme {
  --color-primary: #465fff;
  --color-primary-500: #465fff;
  --color-primary-600: #3641f5;

  --font-sans: "Outfit", sans-serif;

  --spacing-18: 4.5rem;
}

The prefix determines the utility family: --color-* generates color utilities, --font-* font families, --spacing-* spacing, --breakpoint-* responsive variants, --radius-* border radii. A token named --color-brand-muted gives you bg-brand-muted, text-brand-muted and the rest, immediately.

Two consequences that surprise people:

There is no extend any more. A @theme block adds to the defaults. To replace a scale rather than extend it, clear it first with --color-*: initial; and then declare your own.

Your tokens are real CSS variables at runtime. var(--color-primary) works anywhere in your stylesheet, in inline styles, and from JavaScript via getComputedStyle. In v3 those values only existed at build time.

Step 4: Fix dark mode

darkMode: "class" has no direct equivalent. Declare the variant explicitly:

@custom-variant dark (&:where(.dark, .dark *));

Without this line, dark: falls back to prefers-color-scheme and your theme toggle stops working — with no error, which makes it a frustrating one to track down.

Step 5: Rename the changed utilities

The codemod catches most of these, but check them by hand:

v3v4
shadow-smshadow-xs
shadowshadow-sm
rounded-smrounded-xs
roundedrounded-sm
blur-smblur-xs
outline-noneoutline-hidden
ring (3px default)ring is now 1px

The shadow and radius shifts are the dangerous ones because the old names still exist — they just mean something smaller now. Nothing errors; your UI just gets subtly flatter. Grep for shadow-sm and rounded-sm specifically and decide each case.

The ring change is the most visible: v3's bare ring was 3px, v4's is 1px. If your focus states suddenly look thin, that is why. ring-3 restores the old width.

Step 6: Delete the content array

v4 detects template files automatically, respecting .gitignore. Delete content: [...].

If your classes live somewhere unusual — a CMS, a database, a package outside the project — point at it explicitly:

@source "../node_modules/@acme/ui/dist";

Step 7: Audit your plugins

First-party plugins ship as part of v4 or as updated packages. Third-party plugins written against v3's JavaScript plugin API need their v4 releases; some never got one.

Before migrating, list what you depend on:

grep -A20 '"plugins"' tailwind.config.js

Many v3 plugins existed to do things v4 does natively — container queries (@container, built in), text wrapping, and 3D transforms among them. Check whether you still need the dependency at all before hunting for an upgrade.

What the codemod will not do for you

  • Arbitrary values referencing config, like w-[theme(spacing.18)]. These need rewriting against CSS variables: w-[var(--spacing-18)].
  • Runtime config reads. Anything importing tailwind.config.js into application code — a Storybook theme, a design-token export, an email builder — has no config to import any more. Read the CSS variables instead.
  • @apply in component files. Still supported, but @apply in a CSS module or a Vue style block now needs @reference "../globals.css"; at the top so the compiler can see your theme.

That last one produces a confusing error, because the @apply looks correct and simply cannot resolve your custom utilities.

A migration order that works

  1. Branch, then run npx @tailwindcss/upgrade.
  2. Fix the build: PostCSS plugin, imports, dark-mode variant.
  3. Diff the rendered pages, not the source. Shadows and radii shift silently.
  4. Grep for renamed utilities the codemod missed.
  5. Handle plugins last — by then you know which ones you still need.

Budget an afternoon for a mid-sized app, longer if you have custom plugins or generate classes at runtime.

Frequently asked questions

Can I keep using tailwind.config.js? Yes, with @config "./tailwind.config.js" in your CSS. It is a compatibility bridge for incremental migration, not a long-term path — new features assume @theme.

Is v4 faster? Substantially, yes. Full builds are several times quicker and incremental rebuilds are typically in the low milliseconds. The new engine does less work per change.

Do I need to migrate at all? Not urgently. v3 still works. Migrate when you want the performance, container queries, or CSS-variable theming — or when a dependency forces it.

Why is my new color utility not generating? If you are on Turbopack, restart the dev server. @theme changes are not hot-reloaded, so a newly added token produces no CSS until the next cold start.

Starting fresh instead

If you are weighing a migration against a rebuild, our Tailwind admin templates and landing page templates are already built on v4's CSS-first theme, so the token system above is the one you inherit on day one.

Templates in this post

Everything above is how we build ours. If you would rather start from a finished one:

Browse all templates

Keep reading

Tutorial12 min read

How to Build an Admin Dashboard with Next.js 16 and Tailwind CSS v4

A working admin dashboard in Next.js 16 and Tailwind CSS v4 — App Router layouts, a CSS-first theme, an accessible sidebar, and the server/client split that keeps it fast.

Read more