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,@sourceand@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-smis nowshadow-xs,shadowis nowshadow-sm). Review these visually. - Default border color is now
currentColor, andringis 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
contentarray. Tailwind scans your project, respects.gitignoreand 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, andcolor-mix()for opacity modifiers. - New features. Built-in container queries (
@container,@sm:), 3D transforms,bg-linear-*andbg-radial-*gradients, thestarting:variant for@starting-style, and thenot-*variant. - A P3 color palette. Default colors moved from
rgbtooklch, which looks more vivid on wide-gamut screens. - Dynamic values. Utilities like
grid-cols-15ormt-17work 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:
git checkout -b tailwind-v4
npx @tailwindcss/upgradeThe 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):
npm install tailwindcss @tailwindcss/postcss// postcss.config.mjs
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default configYou can remove autoprefixer and postcss-import; v4 handles imports and vendor prefixes itself.
For Vite, the dedicated plugin is faster than going through PostCSS:
npm install tailwindcss @tailwindcss/vite// 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:
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watchStep 3: Move your theme into CSS
Here is a typical v3 config:
// 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:
@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:
.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:
@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:
@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:
<!-- 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
borderwithout a color now usescurrentColorinstead ofgray-200. Dark text means dark borders everywhere.ringis now 1px andcurrentColorinstead of 3px and blue.
If you want the old border look globally while you clean up:
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}Selector and variant changes
space-y-*anddivide-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-4is usually the better choice anyway.- Variant stacking order is now left to right.
first:*:pt-0in v3 becomes*:first:pt-0in 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]becomesbg-(--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,@applycannot see your theme unless you add@reference "../app.css";at the top of that stylesheet. - Monorepos and packages. Automatic detection skips
node_modulesand 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
Keep reading
Next.js8 min read
Next.js App Router vs Pages Router: Migration Guide
Next.js App Router vs Pages Router explained: the real differences, when to migrate, and a step-by-step migration guide updated for Next.js 16.4.
Read articleReact7 min read
10 Custom React Hooks Worth Having (TypeScript Examples)
10 custom React hooks in TypeScript: useDebounce, useLocalStorage, useMediaQuery, useOnClickOutside, useInterval and more, built for React 19 and the compiler.
Read articleReact8 min read
React 19 Hooks: use, useActionState, useOptimistic Explained
React 19 hooks explained with TypeScript examples: use, useActionState, useOptimistic and useFormStatus, plus the gotchas that trip people up in real forms.
Read article