Upgrading from v3 to v4
Tailwind v3 was the standard for years, so you will meet it in job projects, old tutorials and open-source templates. Version 4, released in January 2025, changed how Tailwind is installed and configured, renamed a few utilities and changed some defaults. Most projects upgrade in an afternoon, but only if you know what changed.
In this lesson you will compare a v3 setup with its v4 equivalent side by side, run the official upgrade tool, and learn the renamed classes and changed defaults that cause most visual differences after an upgrade.
Before you upgrade
Check two things first:
- Browser support. v4 uses modern CSS features (cascade layers,
@property,color-mix()) and targets Safari 16.4+, Chrome 111+ and Firefox 128+. If your users need older browsers, stay on v3.4 for now. - Preprocessors. v4 is not designed to be used with Sass, Less or Stylus. Tailwind itself now handles imports, nesting and variables. Plan to move that CSS to plain CSS.
Old and new, side by side
Here is a typical v3 project. The configuration lives in JavaScript:
// tailwind.config.js (v3)
module.exports = {
content: ['./src/**/*.{html,js,php}'],
darkMode: 'class',
theme: {
extend: {
colors: { brand: { 500: '#0284c7', 700: '#0369a1' } },
fontFamily: { display: ['Poppins', 'sans-serif'] },
screens: { '3xl': '1920px' },
},
},
plugins: [require('@tailwindcss/forms')],
};/* src/input.css (v3) */
@tailwind base;
@tailwind components;
@tailwind utilities;In v4 there is no config file. Everything moves into the CSS:
/* src/input.css (v4) */
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-brand-500: #0284c7;
--color-brand-700: #0369a1;
--font-display: "Poppins", sans-serif;
--breakpoint-3xl: 120rem;
}Each piece has a direct replacement:
| v3 | v4 |
|---|---|
@tailwind base; @tailwind components; @tailwind utilities; | @import "tailwindcss"; |
content: [...] | Automatic detection, plus @source when needed |
theme.extend.colors.brand | --color-brand-* in @theme |
darkMode: 'class' | @custom-variant dark (...) |
plugins: [require(...)] | @plugin "..." |
safelist: [...] | @source inline("...") |
Packages tailwindcss + autoprefixer + postcss-import | @tailwindcss/postcss, @tailwindcss/vite or @tailwindcss/cli |
The upgrade tool
Tailwind provides an official tool that does most of the work. It needs Node.js 20 or newer. Run it on a new Git branch so you can review every change:
git switch -c upgrade-tailwind-v4
npx @tailwindcss/upgrade
git diffThe tool updates your packages, moves your config into CSS, replaces the @tailwind directives, and renames changed classes in your templates. Read the diff carefully, then test every page, because some changes are visual rather than syntax. The rest of this lesson tells you what to look for.
If you cannot move the config yet, a v4 project can still load it with @config "../tailwind.config.js"; in the CSS. The corePlugins, safelist and separator options are not supported that way.
Renamed utilities
Several scales gained a new smallest step, so the old names moved down one:
- <div class="shadow-sm rounded-sm blur-sm">
+ <div class="shadow-xs rounded-xs blur-xs">
- <div class="shadow rounded blur">
+ <div class="shadow-sm rounded-sm blur-sm">
- <button class="focus:outline-none focus:ring">
+ <button class="focus:outline-hidden focus:ring-3">
- <div class="bg-gradient-to-r from-sky-500 to-indigo-500">
+ <div class="bg-linear-to-r from-sky-500 to-indigo-500">The same shift applies to drop-shadow and backdrop-blur. Be careful when renaming by hand: shadow-sm exists in both versions but means different sizes, so search-and-replace must go from the smallest name upwards, or you will rename things twice. This is the main reason to use the upgrade tool.
Old deprecated utilities should also be replaced. The opacity utilities (bg-opacity-*, text-opacity-*, border-opacity-* and others) no longer work at all; use the slash modifier, as in bg-black/50. flex-shrink-* becomes shrink-*, flex-grow-* becomes grow-*, and overflow-ellipsis becomes text-ellipsis.
Changed defaults
These do not cause errors, but the page looks different. Run this example and compare each pair:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Changed defaults</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="grid max-w-xl grid-cols-2 gap-4 p-6 text-sm text-slate-900">
<div class="rounded-lg border p-3">v4 <code>border</code>: current text colour</div>
<div class="rounded-lg border border-gray-200 p-3">v3 look: <code>border border-gray-200</code></div>
<button class="rounded-lg p-3 ring">v4 <code>ring</code>: 1px, text colour</button>
<button class="rounded-lg p-3 ring-3 ring-blue-500">v3 look: <code>ring-3 ring-blue-500</code></button>
<input placeholder="v4 placeholder: text colour at 50%" class="rounded-lg border border-gray-300 p-3">
<input placeholder="v3 look: gray-400 placeholder" class="rounded-lg border border-gray-300 p-3 placeholder:text-gray-400">
</body>
</html>The main default changes are:
- Border and divide colour is now
currentColor(v3:gray-200). ringis now 1px incurrentColor(v3: 3px, blue).- Placeholder text is the text colour at 50% opacity (v3:
gray-400). - Buttons use the normal arrow cursor instead of the pointing hand.
hover:only applies on devices that can hover.- Stacked variants apply left to right: v3's
first:*:pt-0is v4's*:first:pt-0. - The important modifier goes at the end:
flex!(the old!flexstill works but is deprecated). - CSS variables in arbitrary values use parentheses:
bg-(--brand)instead ofbg-[--brand]. - A prefix, if you use one, looks like a variant at the start:
tw:flex, set with@import "tailwindcss" prefix(tw);. containerhas nocenterorpaddingoptions; addmx-autoand padding yourself or define it with@utility container.
To bring back a v3 default across the whole project instead of editing every element, add CSS once. For example, for border colours and button cursors:
@layer base {
*, ::before, ::after, ::backdrop {
border-color: var(--color-gray-200, currentColor);
}
button:not(:disabled), [role="button"]:not(:disabled) {
cursor: pointer;
}
}And for the old ring, set --default-ring-width: 3px; and --default-ring-color: var(--color-blue-500); inside @theme.
theme() and JavaScript access
v3 code often used the theme() function in CSS, such as color: theme(colors.sky.700);. In v4, every theme value is a CSS variable, so write color: var(--color-sky-700);. Inside media queries, where variables do not work, v4 accepts theme(--breakpoint-xl). JavaScript that imported resolveConfig to read theme values should read CSS variables with getComputedStyle instead.
Common mistakes
- Upgrading without a branch. Always run the tool on a separate branch and review the diff.
- Renaming shadows by hand in the wrong order.
shadowbecomesshadow-smandshadow-smbecomesshadow-xs; do the smaller ones first or use the tool. - Assuming no visual changes. Check borders, rings, placeholders and hover states on phones after upgrading.
- Keeping Sass around Tailwind. v4 is meant to be used with plain CSS.
- Following v3 tutorials in a v4 project.
npx tailwindcss initand@tailwind baseare v3 instructions.
Key points
- v4 replaces
tailwind.config.jswith CSS:@import "tailwindcss",@theme,@custom-variant,@pluginand@source. - Run
npx @tailwindcss/upgrade(Node.js 20+) on a new branch and review the changes. - Scales shifted: v3
shadow-sm/rounded-sm/blur-smare v4shadow-xs/rounded-xs/blur-xs;ringisring-3;outline-noneisoutline-hidden. - Opacity utilities are gone: use
bg-black/50. - Defaults changed for borders, rings, placeholders, button cursors and hover on touch screens.
- v4 needs Safari 16.4+, Chrome 111+ or Firefox 128+; stay on v3.4 if you must support older browsers.
Exercise
Convert this v3 snippet to v4 by hand. The config adds a colour mango (#f59e0b), sets darkMode: 'class', and the HTML is: <div class="bg-mango bg-opacity-20 shadow rounded border flex-shrink-0 focus:outline-none focus:ring">. Write the v4 CSS file and the updated HTML, keeping the v3 look of the border.
Show solution
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-mango: #f59e0b;
}<div class="bg-mango/20 shadow-sm rounded-sm border border-gray-200 shrink-0 focus:outline-hidden focus:ring-3">bg-opacity-20 becomes the /20 modifier, shadow and rounded move to shadow-sm and rounded-sm, flex-shrink-0 becomes shrink-0, the focus classes become outline-hidden and ring-3, and border-gray-200 restores the v3 border colour.
Quick Quiz
1. What replaces the three @tailwind directives in v4?
One import brings in the theme, base styles and utilities in v4.
2. What is the v4 name for v3's shadow class?
The scale moved down one step: v3 shadow is v4 shadow-sm, and v3 shadow-sm is v4 shadow-xs.
3. How do you safelist classes in v4 now that the safelist config option is gone?
@source inline() forces classes into the build and supports brace expansion.
4. After upgrading, every border looks dark instead of light grey. Why?
Add a colour such as border-gray-200, or set a base border colour once in @layer base.
Learn Tailwind CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
