March 26, 20269 min read

Tailwind CSS v4 — What's New and How to Upgrade from v3

Everything that changed in Tailwind CSS v4: CSS-first configuration, the @theme directive, new color system, container queries, 3D transforms, and a practical v3 migration path.

tailwind css v4 upgrade frontend
Ad 336x280

Tailwind v4 dropped the JavaScript config file. That single change ripples through everything — how you define your design tokens, how you import the framework, how your build pipeline works, and how you think about customization. If you've been writing tailwind.config.js files for years, v4 requires you to unlearn some habits.

The payoff is real though. Builds are dramatically faster (the new Oxide engine is written in Rust), the configuration lives in CSS where it arguably always should have been, and you get features like container queries and 3D transforms out of the box.

Here's what actually changed, what the upgrade looks like, and where things get tricky.

The Big Shift: CSS-First Configuration

In v3, your design system lived in JavaScript:

// tailwind.config.js (v3) — gone in v4
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          500: '#3b82f6',
          900: '#1e3a5a',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      },
      borderRadius: {
        '4xl': '2rem',
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
  ],
}

In v4, all of that moves to your CSS file using the @theme directive:

/ app.css (v4) — this IS your config now /
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5a;

--font-sans: "Inter", "system-ui", sans-serif;
--font-mono: "JetBrains Mono", monospace;

--spacing-18: 4.5rem;
--spacing-88: 22rem;

--radius-4xl: 2rem;
}

No more JavaScript file. No more require(). No more module.exports. Your design tokens are CSS custom properties, which means they're inspectable in DevTools, overridable in media queries, and composable with other CSS tools.

The @theme Directive in Detail

The @theme block is where you define every design token. Tailwind maps these CSS variables to utility classes automatically.

@theme {
  / Colors → generates bg-primary, text-primary, border-primary, etc. /
  --color-primary: #6366f1;
  --color-secondary: #ec4899;
  --color-surface: #f8fafc;
  --color-surface-dark: #1e293b;

/ Typography /
--font-display: "Cal Sans", sans-serif;
--text-xs: 0.75rem;
--text-xs--line-height: 1rem;

/ Shadows — note the nested line-height syntax /
--shadow-soft: 0 2px 8px rgb(0 0 0 / 0.06);
--shadow-hard: 0 4px 12px rgb(0 0 0 / 0.15);

/ Custom breakpoints /
--breakpoint-xs: 30rem;
--breakpoint-3xl: 120rem;

/ Animation /
--animate-fade-in: fade-in 0.3s ease-out;
}

@keyframes fade-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}

Every --color- variable becomes a color utility. Every --spacing- becomes a spacing utility. The naming convention is the mapping — no configuration object needed.

Overriding Default Theme Values

Want to wipe out Tailwind's default color palette and use only yours?

@theme {
  --color-: initial; / Clears ALL default colors */

--color-black: #000;
--color-white: #fff;
--color-brand: #6366f1;
--color-muted: #94a3b8;
--color-danger: #ef4444;
}

The --color-*: initial syntax resets the entire namespace. This replaces v3's theme (non-extend) overrides.

Import Changes

v3 used three directives:

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

v4 uses a single import:

/ v4 /
@import "tailwindcss";

That one line pulls in the base reset, all utilities, and the theme. If you need to add custom base styles or component classes, use @layer:

@import "tailwindcss";

@layer base {
html {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
}

@layer components {
.btn-primary {
@apply rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white
hover:bg-primary/90 focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-primary;
}
}

New Color System

v4 moves to OKLCH as the default color space. The entire palette was regenerated for perceptual uniformity — meaning blue-500 and red-500 actually look like they have the same visual weight now, which was never quite true in v3's HSL-derived palette.

You won't notice this in most cases. The class names are identical (bg-blue-500, text-red-700). But if you've hardcoded hex values that matched v3's palette, they'll be slightly different.

/ The default palette uses OKLCH internally /
/ You can still define custom colors in any format /
@theme {
  --color-brand: oklch(0.65 0.2 260);     / OKLCH /
  --color-accent: #f59e0b;                 / Hex /
  --color-surface: rgb(248 250 252);       / RGB /
  --color-overlay: hsl(220 14% 10% / 0.8); / HSL with alpha /
}

Container Queries — Built In

This was a plugin in v3. Now it's native. Container queries let components respond to their parent's size instead of the viewport — which is what you actually want most of the time for reusable components.

<!-- Mark an element as a container -->
<div class="@container">
  <div class="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3 gap-4">
    <div class="p-4 @sm:p-6">
      <h3 class="text-base @md:text-lg font-semibold">Card Title</h3>
      <p class="hidden @sm:block text-muted">
        Only shows when the container is wide enough
      </p>
    </div>
  </div>
</div>

The @ prefix differentiates container queries from viewport breakpoints. @md means "when my container is at least md-width," not "when the viewport is."

You can also name containers:

<div class="@container/sidebar">
  <nav class="@lg/sidebar:flex-row flex flex-col gap-2">
    <!-- Responds to sidebar container width -->
  </nav>
</div>

3D Transforms

v4 adds first-class 3D transform utilities. No more fighting with arbitrary values for perspective and rotation.

<!-- Card flip effect -->
<div class="group perspective-1000">
  <div class="relative transition-transform duration-500
              transform-3d group-hover:rotate-y-180">
    <!-- Front -->
    <div class="absolute inset-0 backface-hidden rounded-xl bg-white p-6 shadow-lg">
      <h3>Front of card</h3>
    </div>
    <!-- Back -->
    <div class="absolute inset-0 backface-hidden rotate-y-180
                rounded-xl bg-indigo-600 p-6 text-white shadow-lg">
      <h3>Back of card</h3>
    </div>
  </div>
</div>

Key new utilities:

UtilityWhat it does
perspective-*Sets CSS perspective on parent
rotate-x-, rotate-y-3D rotation around X/Y axis
translate-z-*Movement along Z axis
transform-3dEnables transform-style: preserve-3d
backface-hiddenHides back face of rotated elements
scale-z-*Scale along Z axis

Build Performance: The Oxide Engine

v4's compiler is written in Rust (the Oxide engine) and processes CSS significantly faster than v3's JavaScript-based engine. On a large project with thousands of utility usages, rebuild times can drop from hundreds of milliseconds to single-digit milliseconds.

The practical impact: hot reload in development is nearly instant. Full production builds that took 3-4 seconds now complete in under a second.

PostCSS Setup Changes

v3:

// postcss.config.js (v3)
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

v4:

// postcss.config.js (v4)
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

The package name changed. Autoprefixer is built in — you don't need it as a separate dependency anymore.

Upgrading from v3 — Step by Step

1. Update Dependencies

npm install tailwindcss@4 @tailwindcss/postcss@4
npm uninstall autoprefixer  # Built into v4

# If you use plugins:
npm install @tailwindcss/typography@4 @tailwindcss/forms@4

2. Update PostCSS Config

// postcss.config.js
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

3. Migrate Your CSS Entry Point

Replace the three directives with a single import, then move your config:

/ Before (v3) /
@tailwind base;
@tailwind components;
@tailwind utilities;

/ After (v4) /
@import "tailwindcss";

4. Move tailwind.config.js to @theme

This is the manual part. Take your theme.extend values and convert them to CSS variables:

// v3 config (delete after migration)
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: '#6366f1',
        surface: { light: '#f8fafc', dark: '#0f172a' },
      },
      spacing: { '18': '4.5rem' },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
    },
  },
}

Becomes:

@theme {
  --color-brand: #6366f1;
  --color-surface-light: #f8fafc;
  --color-surface-dark: #0f172a;
  --spacing-18: 4.5rem;
  --font-sans: "Inter", sans-serif;
}

5. Update Plugin Imports

/ v3: plugins were in tailwind.config.js /
/ v4: plugins go in CSS /
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

6. Use the Automated Upgrade Tool

Tailwind provides a codemod to handle most of this automatically:

npx @tailwindcss/upgrade

This scans your project, migrates the config, updates CSS files, and adjusts class names for any breaking changes. It handles maybe 80% of the migration. The remaining 20% is usually custom plugin migrations and edge cases with arbitrary values.

7. Check for Breaking Class Name Changes

Some utilities were renamed or removed:

v3v4Notes
decoration-clonebox-decoration-cloneFull name now
decoration-slicebox-decoration-sliceFull name now
flex-growgrowShortened (both still work)
flex-shrinkshrinkShortened (both still work)
overflow-ellipsistext-ellipsisRenamed
bg-opacity-*bg-black/50Opacity modifier syntax preferred
The opacity utility classes (bg-opacity-, text-opacity-, etc.) still work but the slash modifier syntax (bg-black/50) is strongly preferred.

When NOT to Upgrade

Hold off on v4 if:

  • You depend on v3-only plugins that haven't been updated. Check your plugin list first.
  • You have a massive custom plugin with addUtilities/addComponents — the plugin API changed, and your plugin needs rewriting.
  • Your build pipeline does unusual things with the Tailwind config (reading it programmatically, generating it dynamically). The JS config is gone, so those patterns break.
  • You're mid-sprint on a deadline. The migration is straightforward but not zero-effort. Budget a day for a large project.
For new projects, there's no reason to start with v3 anymore. v4 is the way forward.

Real-World Migration Notes

After migrating several projects at codeup.dev, a few things stood out:

  1. The codemod misses @apply in component libraries. If you have a shared UI library using @apply heavily, review those files manually after the migration.
  1. Dark mode configuration changed. If you were using darkMode: 'class' in v3, you'll need to adjust. v4 uses the CSS prefers-color-scheme media query by default, and you enable class-based dark mode with a variant:
@variant dark (&:where(.dark, .dark *));
  1. Content paths are automatic. v4 detects your source files automatically — no more content: ['./src/*/.{js,ts,jsx,tsx}'] configuration. It scans everything in your project directory.
  1. JIT is the only mode. This was already the default in v3, but v4 removes the classic engine entirely. Every build is JIT.

Quick Reference: v3 vs v4

Featurev3v4
Config formatJavaScript (tailwind.config.js)CSS (@theme directive)
Import syntax@tailwind base/components/utilities@import "tailwindcss"
Plugin loadingrequire() in JS config@plugin in CSS
Color spaceHSL-derivedOKLCH
Container queriesPlugin (@tailwindcss/container-queries)Built in (@container)
3D transformsArbitrary values onlyFirst-class utilities
Build engineJavaScriptRust (Oxide)
AutoprefixerSeparate dependencyBuilt in
Content detectionManual paths in configAutomatic
PostCSS packagetailwindcss@tailwindcss/postcss
The migration is worth doing. Faster builds, simpler configuration, and features that were previously plugin-only or impossible. Just don't try to do it on a Friday afternoon.
Ad 728x90