Skip to content

CSS7 min read

Tailwind CSS v4 Migration Guide: What Changed From v3

Tailwind CSS v4 migration guide: CSS-first config with @theme, the upgrade tool, renamed utilities, browser support and the breaking changes that bite.

Tailwind CSS v4 moves configuration from tailwind.config.js into your CSS file. You replace the three @tailwind directives with a single @import "tailwindcss";, define design tokens inside an @theme block, and install a dedicated plugin for your build tool (@tailwindcss/postcss or @tailwindcss/vite). The fastest way to migrate from v3 is npx @tailwindcss/upgrade, followed by a careful review of a handful of breaking changes around borders, rings, shadows and variant order.

Tailwind v4.0 shipped in January 2025, and the v4 line has kept getting minor releases since. This guide covers what actually changed, the upgrade steps for Next.js and Vite, and the gotchas I would check before merging.

Key takeaways

  • Configuration is CSS-first: @import "tailwindcss" plus @theme, @utility, @custom-variant, @source and @plugin.
  • The official upgrade tool handles most of the work. It requires Node.js 20+.
  • v4 targets Safari 16.4+, Chrome 111+ and Firefox 128+. Older browsers mean staying on v3.4.
  • Several utilities were renamed one step down the scale (shadow-sm is now shadow-xs, shadow is now shadow-sm). Review these visually.
  • Default border color is now currentColor, and ring is 1px by default. These cause most "something looks off" bugs.

What changed in Tailwind CSS v4

The headline changes from the v4.0 release:

  • CSS-first configuration. Your theme lives in CSS. Every token is also exposed as a native CSS variable, so var(--color-brand-500) works anywhere.
  • Automatic content detection. No content array. Tailwind scans your project, respects .gitignore and skips binary files. You add extra paths with @source.
  • A new engine. The Tailwind team's own benchmarks show full builds several times faster and incremental builds much faster than v3.4.
  • Modern CSS underneath. Native cascade layers, registered custom properties with @property, and color-mix() for opacity modifiers.
  • New features. Built-in container queries (@container, @sm:), 3D transforms, bg-linear-* and bg-radial-* gradients, the starting: variant for @starting-style, and the not-* variant.
  • A P3 color palette. Default colors moved from rgb to oklch, which looks more vivid on wide-gamut screens.
  • Dynamic values. Utilities like grid-cols-15 or mt-17 work without extending the theme.

Tailwind v3 vs v4 at a glance

Area v3 v4
Entry point @tailwind base; @tailwind components; @tailwind utilities; @import "tailwindcss";
Config tailwind.config.js @theme in CSS (JS config only via @config)
Content paths content: [...] Automatic, plus @source
PostCSS plugin tailwindcss + autoprefixer @tailwindcss/postcss (prefixing built in)
Vite PostCSS @tailwindcss/vite plugin
CLI tailwindcss @tailwindcss/cli
Custom utilities @layer utilities { ... } @utility name { ... }
Dark mode by class darkMode: 'class' @custom-variant dark (...)
Plugins plugins: [require(...)] @plugin "...";
Important !flex flex!
CSS variable shorthand bg-[--brand] bg-(--brand)

How to upgrade from Tailwind v3 to v4

Step 1: Run the upgrade tool

On a fresh branch with a clean working tree:

Terminal
git checkout -b tailwind-v4
npx @tailwindcss/upgrade

The tool updates package.json, converts your JS config into CSS where it can, replaces the @tailwind directives and rewrites renamed classes in your templates. It needs Node.js 20 or newer. Read the diff file by file; it is good, but it cannot know whether a shadow on a card was meant to look exactly the same.

Step 2: Update the build setup

For Next.js (or anything using PostCSS):

Terminal
npm install tailwindcss @tailwindcss/postcss
JavaScript
// postcss.config.mjs
const config = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

export default config

You can remove autoprefixer and postcss-import; v4 handles imports and vendor prefixes itself.

For Vite, the dedicated plugin is faster than going through PostCSS:

Terminal
npm install tailwindcss @tailwindcss/vite
TypeScript
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

If you compile with the CLI, the binary moved to its own package:

Terminal
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch

Step 3: Move your theme into CSS

Here is a typical v3 config:

JavaScript
// tailwind.config.js (v3)
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        brand: {
          500: '#2563eb',
          600: '#1d4ed8',
        },
      },
      fontFamily: {
        display: ['Inter', 'sans-serif'],
      },
      screens: {
        '3xl': '1920px',
      },
    },
  },
  plugins: [require('@tailwindcss/typography')],
}

And the v4 equivalent, all in your main CSS file:

CSS
@import "tailwindcss";

@plugin "@tailwindcss/typography";

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

@theme {
  --color-brand-500: #2563eb;
  --color-brand-600: #1d4ed8;
  --font-display: "Inter", sans-serif;
  --breakpoint-3xl: 1920px;
}

The variable name decides which utilities you get. --color-brand-500 creates bg-brand-500, text-brand-500, border-brand-500 and so on. --font-display creates font-display. --breakpoint-3xl creates the 3xl: variant. Values in @theme extend the defaults; to replace a whole namespace, reset it first with something like --color-*: initial;.

Because the tokens are real CSS variables, you can use them outside utility classes:

CSS
.chart-tooltip {
  border-color: var(--color-brand-500);
}

That also replaces most uses of v3's theme() function and resolveConfig, both of which are gone or discouraged in v4.

Step 4: Port custom utilities and components

@layer utilities and @layer components blocks become @utility:

CSS
@utility content-auto {
  content-visibility: auto;
}

@utility container {
  margin-inline: auto;
  padding-inline: 1rem;
}

Custom utilities registered this way work with variants (hover:content-auto, md:container), which plain CSS classes in a layer do not. The second example also shows how to replace the old container.center and container.padding options, which v4 removed.

If you have files that still need the old JS config for now, load it explicitly:

CSS
@import "tailwindcss";
@config "../../tailwind.config.js";

corePlugins, safelist and separator are not supported in v4. For safelisting, use @source inline("...").

Tailwind v4 breaking changes to check by hand

These are the changes the upgrade tool cannot fully judge for you.

Renamed scale utilities

v3 class v4 class
shadow-sm shadow-xs
shadow shadow-sm
rounded-sm rounded-xs
rounded rounded-sm
blur-sm blur-xs
blur blur-sm
outline-none outline-hidden
ring ring-3

The upgrade tool renames these, but if you build class names dynamically ('shadow' + size), it will miss them. Search for string concatenation around class names.

Removed opacity utilities

bg-opacity-*, text-opacity-*, border-opacity-* and friends are gone. Use the slash modifier:

HTML
<!-- v3 -->
<div class="bg-black bg-opacity-50"></div>

<!-- v4 -->
<div class="bg-black/50"></div>

flex-shrink-* and flex-grow-* became shrink-* and grow-*.

Border and ring defaults

  • border without a color now uses currentColor instead of gray-200. Dark text means dark borders everywhere.
  • ring is now 1px and currentColor instead of 3px and blue.

If you want the old border look globally while you clean up:

CSS
@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }
}

Selector and variant changes

  • space-y-* and divide-y-* now use :not(:last-child) instead of the sibling selector, so the spacing sits on the bottom of each item. Inline elements and lists with hidden items can look different. flex flex-col gap-4 is usually the better choice anyway.
  • Variant stacking order is now left to right. first:*:pt-0 in v3 becomes *:first:pt-0 in v4.
  • hover: now only applies on devices that support hover (@media (hover: hover)), so tap-to-hover states on phones disappear. Usually this is an improvement.
  • Arbitrary CSS variables use parentheses: bg-[--brand-color] becomes bg-(--brand-color).
  • Prefixes look like variants and come first: tw:flex tw:hover:bg-red-600.

Preflight changes

Buttons now use cursor: default instead of pointer, placeholders use the current text color at 50% opacity, and the hidden attribute wins over display utilities. If your buttons suddenly lose the pointer cursor, add it back in a base layer.

Gotchas I would watch for

  • Sass and Less are not supported with v4. Tailwind acts as the preprocessor. If your project depends on Sass, plan that migration separately or stay on v3.
  • CSS modules and component styles need @reference. In Vue, Svelte, Astro or CSS modules, @apply cannot see your theme unless you add @reference "../app.css"; at the top of that stylesheet.
  • Monorepos and packages. Automatic detection skips node_modules and anything in .gitignore. If a shared UI package lives there, add @source "../node_modules/@acme/ui";.
  • Browser support is a business decision. If analytics show meaningful traffic on older Safari versions, do not upgrade yet.
  • Visual regression. The safest review I know is a quick screenshot pass of key pages before and after. Most issues are borders, shadows and spacing, and they are obvious side by side.

Tailwind pairs well with whichever framework you choose. If you are picking a stack now, see my comparison of Vite, Next.js and React Router. Smaller CSS also helps paint times, which I cover in Core Web Vitals for React apps. The official Tailwind upgrade guide has the complete list of changes.

FAQ

How do I upgrade from Tailwind CSS v3 to v4?

Run npx @tailwindcss/upgrade on a clean Git branch. It needs Node.js 20 or newer, updates dependencies, moves your config into CSS, and rewrites renamed classes in your templates. Then review the diff and test the UI visually.

Does Tailwind v4 still use tailwind.config.js?

Not by default. v4 is configured in CSS with @theme and other directives, and a JavaScript config is no longer detected automatically. You can still load one explicitly with @config, but corePlugins, safelist and separator are not supported.

What browsers does Tailwind CSS v4 support?

Tailwind v4 targets Safari 16.4+, Chrome 111+ and Firefox 128+, because it relies on modern CSS like cascade layers, @property and color-mix(). If you must support older browsers, stay on v3.4.

Why did my borders change color after upgrading to Tailwind v4?

In v4, border and divide utilities default to currentColor instead of gray-200. Add an explicit color like border-gray-200, or restore the old default with a small base-layer rule.

Wrapping up

Tailwind v4 is mostly a better way to configure the same utilities you already know. Run the upgrade tool on a branch, move your tokens into @theme, then spend your review time on borders, rings, shadows and variant order. Once it is done, having your design tokens as plain CSS variables is the part you will appreciate most.

  • #tailwindcss
  • #css
  • #migration
  • #frontend