Tailwind CSS v4 Migration Guide: From v3 Config to CSS-First
Tailwind v4 drops tailwind.config.js entirely. Here's the exact migration path from v3's JS config to v4's CSS-first approach — no guesswork, just code.
Why v4 Breaks Everything You Knew
Tailwind CSS v4.0, released in early 2025, is not a minor version bump. It rewrites the configuration model from the ground up. If you've been shipping with v3 for the past few years, your first instinct when you npm install tailwindcss@latest and stare at a blank screen will be to check if something went wrong. Nothing went wrong — your tailwind.config.js is just gone now, and that's intentional.
The core shift is from JavaScript-based configuration to what the team calls "CSS-first config." Instead of a module.exports object with a theme.extend block, you define your design tokens directly inside your CSS file using native CSS custom properties and a new @theme at-rule. The tooling rebuilt around a new high-performance Rust-based engine (previously called Oxide) that can scan your entire project in milliseconds. That speed-up is real — builds that took 800ms in v3 routinely finish under 100ms in v4.
Worth noting: Tailwind's PostCSS plugin still exists, but it's no longer the recommended path for new projects. The new @tailwindcss/vite plugin hooks directly into Vite's transform pipeline, which cuts full-build times even further. If you're on Next.js, there's @tailwindcss/postcss — the API is the same, just a different package name.
Honestly, the migration feels bigger than it is. Once you understand that everything that used to live in tailwind.config.js now lives in your CSS file, the mental model snaps into place pretty quickly. Let's walk through it piece by piece.
Step 1: Update Dependencies and Remove the Config File
Start by updating your packages. In v3 you had tailwindcss, postcss, and autoprefixer as dev dependencies. In v4, autoprefixer is bundled — you can remove it. The install looks like this:
``bash
# Remove old packages
npm uninstall tailwindcss autoprefixer
# Install v4
npm install -D tailwindcss@latest @tailwindcss/vite
# OR for PostCSS setups (Next.js, etc.)
npm install -D tailwindcss@latest @tailwindcss/postcss
``
If you're on Vite, update vite.config.ts to use the new plugin:
``ts
// vite.config.ts
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
tailwindcss(),
],
});
`
For Next.js, update postcss.config.mjs:
`js
// postcss.config.mjs
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
``
Now delete tailwind.config.js (or .ts). Also delete postcss.config.js if you were only using it for Tailwind and autoprefixer. That's it — no config file, no content array, no theme object. v4 auto-detects your source files using an opinionated heuristic that scans your entire project directory, excluding node_modules and build output. You can override this with @source directives in your CSS if needed, but most projects won't have to.
Step 2: Migrate Your CSS Entry Point
In v3, your main CSS file probably looked like this:
``css
/* v3 — globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
`
In v4, that becomes a single import:
`css
/* v4 — globals.css */
@import "tailwindcss";
`
That one line pulls in base styles, all utility classes, and the new v4 defaults. Quick aside: if you had a @layer base block for font-face declarations or CSS resets, those still work exactly as before — the @layer` directive is unchanged.
If you're using the PostCSS setup and your build tool doesn't support CSS @import natively, you may need postcss-import to resolve the import at build time. Add it before @tailwindcss/postcss in your PostCSS config:
``js
export default {
plugins: {
'postcss-import': {},
'@tailwindcss/postcss': {},
},
};
``
One more thing — if you had a @tailwind components layer with hand-written component classes, those move to @layer components blocks inside your CSS file. The semantics haven't changed; only the entrypoint directive did. Everything you wrote inside @layer blocks in v3 migrates without edits.
Step 3: Migrate Your Theme — The Big One
This is where most of your migration time will go. In v3, custom colors, fonts, spacing, and breakpoints lived in tailwind.config.js under theme.extend. In v4 they live in a @theme block inside your CSS file. The values become native CSS custom properties that Tailwind reads and converts into utility classes.
Here's a direct comparison:
``js
// v3 — tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
DEFAULT: '#6C3AFA',
light: '#8B5CF6',
dark: '#4C1D95',
},
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Cal Sans', 'sans-serif'],
},
spacing: {
18: '4.5rem',
128: '32rem',
},
},
},
};
`
`css
/* v4 — globals.css */
@import "tailwindcss";
@theme {
--color-brand: #6C3AFA;
--color-brand-light: #8B5CF6;
--color-brand-dark: #4C1D95;
--font-sans: 'Inter', sans-serif;
--font-display: 'Cal Sans', sans-serif;
--spacing-18: 4.5rem;
--spacing-128: 32rem;
}
``
The naming convention follows a pattern: --{category}-{key}. Colors become --color-*, font families become --font-*, spacing becomes --spacing-*, breakpoints become --breakpoint-*. This maps to the same utility class names you used in v3 — so bg-brand, font-display, p-18 all still work. You're not rewriting your HTML.
Custom breakpoints get the same treatment:
``css
@theme {
--breakpoint-xs: 30rem; /* 480px */
--breakpoint-3xl: 112rem; /* 1792px */
}
`
This replaces the old screens key in theme.extend. In practice, if your breakpoint names don't conflict with v4's defaults (sm, md, lg, xl, 2xl`), you can add them alongside without any collisions.
If you had a lot of theme extensions, Tailwind ships an official codemod that handles most of the mechanical conversion:
``bash
npx @tailwindcss/upgrade@latest
`
Run it, review the diff, and fix anything it missed. It won't catch everything — especially custom plugin logic — but it handles the theme.extend → @theme` conversion pretty reliably. Building visual design systems like the ones behind Empire UI is much cleaner in v4 because your design tokens live right next to your component styles, not in a separate JS file.
Step 4: Migrate Plugins and Arbitrary Values
Tailwind's first-party plugins (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) are now imported as CSS, not registered in a JS config array:
``css
/* v4 */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
`
That's a genuinely nicer API. No more chasing down the plugins` array in your config and importing packages there.
For custom plugins you wrote yourself — functions that added utilities or components via addUtilities or addComponents — those still work, but they now get registered with @plugin pointing to a local JS file:
``css
@plugin "./plugins/text-stroke.js";
`
The plugin API itself is unchanged, so your existing plugin code should mostly just work. That said, if your plugin was reading from theme()` callbacks to derive values, test it carefully — the theme resolution internals changed.
Arbitrary values like w-[calc(100%-48px)] and text-[14px] are fully supported and work identically to v3. The square-bracket syntax didn't change. One area that did change slightly: theme() inside CSS — in v4 you reference var(--spacing-4) directly instead of calling theme('spacing.4'). The theme() function still works for backward compat, but it's considered legacy now. Look, if you've been building glassmorphism components or anything with heavy custom backdrop-blur values, you'll want to switch to CSS variables — the performance is better and the syntax is cleaner.
Third-party community plugins vary. Check each plugin's release notes for v4 compatibility before migrating. Some popular ones (like tailwind-scrollbar and tailwindcss-animate) already ship v4-compatible versions as of mid-2025. Others are lagging. If a plugin isn't updated yet, you can usually replicate its functionality with a small @plugin file or a handful of @utility declarations directly in your CSS.
Handling v4 Breaking Changes in Utility Classes
A handful of utility class names changed between v3 and v4. The most common ones you'll hit:
- shadow-sm → renamed, same output, no change needed
- ring-1 → unchanged
- overflow-ellipsis → now text-ellipsis (officially deprecated since v3, now removed)
- decoration-clone → box-decoration-clone
- decoration-slice → box-decoration-slice
- flex-shrink-0 → shrink-0 (this was already aliased in late v3)
- flex-grow → grow
The codemod handles most of these. Run it before doing any manual cleanup.
The default border color changed. In v3, border by itself applied a 1px solid currentColor-adjacent gray (border-gray-200). In v4, border applies 1px solid var(--color-gray-200) — functionally the same but via CSS variable, which means you can override it at the theme level. If you were relying on the implicit border color without specifying a color class, visually nothing will look different. But if you're overriding --color-gray-200 in your @theme, be aware it now affects bare border too.
The gradient generator on Empire UI is a good sanity check after migration — paste in your gradient utility classes and see if the output still matches what you expect. Gradient utilities like bg-gradient-to-br from-violet-600 to-fuchsia-500 are unchanged in v4, but from-[#6C3AFA] arbitrary color values render slightly differently in some edge cases involving color spaces. v4 defaults to the oklch color space for generated palette colors, which means the same hex value you pass as an arbitrary value may not exactly match a named color at the same position in the scale.
In practice, most teams won't notice the color space difference on screen. It's most visible when you're mixing arbitrary hex values with named scale colors in the same gradient and expecting them to blend smoothly.
v4 in Production: What Actually Changed About DX
After migrating a few projects, here's what genuinely improved: the feedback loop is faster. Hot module replacement in Vite with the native plugin is near-instant. You edit a @theme variable, save, and the browser updates in under 50ms. No more waiting for PostCSS to regenerate a large stylesheet. For a component library like Empire UI, that speed difference compounds — when you're iterating on 400+ components, shaving 700ms per save adds up fast.
Source detection is better. In v3, forgetting to add a path to content in your config would silently result in missing utility classes in production. You'd get baffled by a class that works in dev but disappears after build. v4's auto-detection scans everything, so that entire class of bug is gone. It does mean slightly larger dev-server startup times on very large monorepos, but you can add @source directives to constrain the scan if needed:
``css
@import "tailwindcss";
@source "./src";
@source "./packages/ui/src";
``
CSS variables as first-class citizens is the best long-term improvement. Your theme is now real CSS — you can read var(--color-brand) in plain CSS rules, in inline styles, in canvas contexts, in JavaScript via getComputedStyle. The design tokens you define in @theme are just CSS variables accessible everywhere. That's something you couldn't cleanly do with the old JS config without running a separate token extraction script. Pair that with the box shadow generator and a @theme block for your elevation scale, and you've got a token system that actually integrates with every layer of your stack.
One more thing — IDE support in 2026 is solid. The Tailwind CSS IntelliSense extension (v0.14+) understands @theme blocks and autocompletes your custom tokens in HTML class attributes. If you're on an older version of the extension, update it first before wondering why autocomplete isn't working for your custom colors. It's a known gotcha that wastes about 20 minutes if you don't know to look for it.
FAQ
Yes, in v4 the JS config file is no longer supported. All your theme customizations move to a @theme block in your CSS file. The official codemod (npx @tailwindcss/upgrade) handles most of the conversion automatically.
Mostly yes — the vast majority of utility class names are identical. A small set of deprecated aliases from v3 (like flex-shrink-0 and overflow-ellipsis) were removed in v4, but the codemod finds and rewrites them for you.
Yes, via the @tailwindcss/postcss package. The Vite plugin is recommended for new Vite projects, but PostCSS still works and is the right choice for Next.js and other non-Vite setups.
It's gone — v4 auto-detects your source files by scanning the project directory. You can use @source directives in your CSS to narrow the scan scope on large monorepos, but most projects don't need to.