Upgrading from v3 to v4

Tailwind CSS · Lesson 26 of 27 7 min read Updated

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:

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')],
};
CSS
/* src/input.css (v3) */
@tailwind base;
@tailwind components;
@tailwind utilities;

In v4 there is no config file. Everything moves into the CSS:

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:

v3v4
@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:

Terminal
git switch -c upgrade-tailwind-v4
npx @tailwindcss/upgrade
git diff

The 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:

Diff
- <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:

HTMLTry it yourself
<!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).
  • ring is now 1px in currentColor (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-0 is v4's *:first:pt-0.
  • The important modifier goes at the end: flex! (the old !flex still works but is deprecated).
  • CSS variables in arbitrary values use parentheses: bg-(--brand) instead of bg-[--brand].
  • A prefix, if you use one, looks like a variant at the start: tw:flex, set with @import "tailwindcss" prefix(tw);.
  • container has no center or padding options; add mx-auto and 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:

CSS
@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. shadow becomes shadow-sm and shadow-sm becomes shadow-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 init and @tailwind base are v3 instructions.

Key points

  • v4 replaces tailwind.config.js with CSS: @import "tailwindcss", @theme, @custom-variant, @plugin and @source.
  • Run npx @tailwindcss/upgrade (Node.js 20+) on a new branch and review the changes.
  • Scales shifted: v3 shadow-sm/rounded-sm/blur-sm are v4 shadow-xs/rounded-xs/blur-xs; ring is ring-3; outline-none is outline-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
CSS
@import "tailwindcss";

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

@theme {
  --color-mango: #f59e0b;
}
HTML
<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?

2. What is the v4 name for v3's shadow class?

3. How do you safelist classes in v4 now that the safelist config option is gone?

4. After upgrading, every border looks dark instead of light grey. Why?

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