Tailwind CSS Handbook v4.3

Deprecated APIs

Every utility, directive, config option and package removed, renamed or deprecated in Tailwind v4 — what replaced it, and how to find it in your codebase.

A lookup table for anything that stopped working, changed meaning, or is on its way out. Use it when a v3 snippet from a blog post does not do what the author promised — most of the time the class still compiles, it just means something else now.

The dangerous entries are marked. A removed class produces no CSS, which you notice immediately. A renamed class where the old name still exists produces the wrong CSS, which you may not notice for weeks.

Opacity utilities#

Removed The *-opacity-* family is gone. Every colour utility takes a / opacity modifier instead.

RemovedReplacement
bg-opacity-*bg-black/50
text-opacity-*text-black/50
border-opacity-*border-black/50
divide-opacity-*divide-black/50
ring-opacity-*ring-black/50
placeholder-opacity-*placeholder-black/50
HTML
<!-- v3 — two classes, and the opacity is a separate axis -->
<div class="bg-indigo-500 bg-opacity-50 hover:bg-opacity-75">…</div>

<!-- v4 — one class, composes with variants and arbitrary colours -->
<div class="bg-indigo-500/50 hover:bg-indigo-500/75">…</div>

The old family existed because v3 implemented colours as an RGB triple plus a separate --tw-bg-opacity variable. v4 uses color-mix(), so the modifier works on any colour, including bg-(--brand)/40.

Flex utilities#

Removed The flex- prefix was dropped from grow and shrink.

RemovedReplacement
flex-growgrow
flex-grow-0grow-0
flex-shrinkshrink
flex-shrink-0shrink-0
HTML
<!-- v3 -->
<div class="flex-grow flex-shrink-0">…</div>

<!-- v4 -->
<div class="grow shrink-0">…</div>

flex-1, flex-auto, flex-initial and flex-none are unchanged — they set the flex shorthand, not the individual longhands.

Text overflow#

Removed

RemovedReplacement
overflow-ellipsistext-ellipsis

text-ellipsis and text-clip had already existed alongside overflow-ellipsis in v3; v4 simply dropped the duplicate. Nothing about the behaviour changed — text-ellipsis still needs overflow-hidden and whitespace-nowrap to do anything.

Box decoration#

Removed

RemovedReplacement
decoration-slicebox-decoration-slice
decoration-clonebox-decoration-clone

The short names conflicted with the decoration-* text-decoration utilities (decoration-2, decoration-indigo-500, decoration-wavy), which are not deprecated. Only the two box-decoration aliases were removed.

The size-scale renames#

Renamed — old names still compile

This is the most important section on the page. Five scales were shifted so that every step has an explicit name, with no unnamed bare utility sitting between xs and md.

v3v4
shadow-smshadow-xs
shadowshadow-sm
drop-shadow-smdrop-shadow-xs
drop-shadowdrop-shadow-sm
blur-smblur-xs
blurblur-sm
backdrop-blur-smbackdrop-blur-xs
backdrop-blurbackdrop-blur-sm
rounded-smrounded-xs
roundedrounded-sm
HTML
<!-- Written for v3: a medium shadow and a 2px radius -->
<div class="rounded shadow">…</div>

<!-- The same visual result in v4 -->
<div class="rounded-sm shadow-sm">…</div>

Reading the mapping in reverse is the useful direction: if you are looking at v3 code, add a step. If you are looking at v4 code, it means what it says.

The larger steps — shadow-md, shadow-lg, shadow-xl, shadow-2xl, rounded-md and up — are unchanged.

Outline#

outline-none#

Meaning changed The class still exists in v4 and does something different.

Classv3v4
outline-noneoutline: 2px solid transparent — invisible in normal rendering, but still drawn in forced-colours modeoutline-style: none — the outline is genuinely removed
outline-hiddendid not existthe behaviour v3 called outline-none
HTML
<!-- v3: hide the default outline, draw your own ring -->
<button class="focus:outline-none focus:ring-2 focus:ring-indigo-500">…</button>

<!-- v4: same intent, and still visible in Windows High Contrast mode -->
<button class="focus:outline-hidden focus:ring-2 focus:ring-indigo-500">…</button>

Use outline-none deliberately, and only when nothing else supplies the focus indicator.

outline on its own#

Behaviour changed In v3, outline set outline-style: solid and you paired it with a width. In v4 the width utilities imply the style, so the pairing is unnecessary.

HTML
<!-- v3 -->
<div class="outline outline-2 outline-indigo-500">…</div>

<!-- v4 -->
<div class="outline-2 outline-indigo-500">…</div>

Ring#

Default changed

v3v4
Bare ring width3px1px
Default ring colourblue-500currentColor

ring-3 reproduces the v3 width, and the colour must now be stated:

HTML
<!-- v3 -->
<button class="ring">…</button>

<!-- Identical output in v4 -->
<button class="ring-3 ring-blue-500">…</button>

The v3 default was a debugging convenience that shipped to production in a lot of codebases. v4's currentColor default follows the same rule as border, but it means a bare ring on dark text is a thin dark line — easy to miss as a focus indicator. State the width and the colour.

Gradients#

Renamed — alias retained

v3v4
bg-gradient-to-rbg-linear-to-r
bg-gradient-to-brbg-linear-to-br
bg-gradient-to-*bg-linear-to-*

The rename made room for bg-radial-* and bg-conic-*, which are new in v4. The old names still work as deprecated aliases, so this is the one rename with no visual consequence — but new code should use bg-linear-*.

There is also a behaviour change with no rename attached: gradient stops now persist across variants. In v3 a dark: override reset the stops you did not mention; in v4 they are preserved, so a three-stop gradient stays three-stop unless you reset it:

HTML
<div class="bg-linear-to-r from-red-500 via-orange-400 to-yellow-400
            dark:from-blue-500 dark:via-none dark:to-teal-400">…</div>

via-none is the explicit reset back to a two-stop gradient.

Configuration file options#

Removed There is no config file in v4. @config still loads one, but several keys are ignored inside it — silently.

v3 optionv4
contentAutomatic source detection, plus @source for anything outside it
theme@theme in CSS
theme.extend@theme — declaring a value adds it; the namespace is only replaced if you reset it with initial
plugins@plugin "…"
prefix: 'tw-'@import "tailwindcss" prefix(tw); — and the prefix moves to the front as a variant: tw:flex
darkMode@custom-variant dark (…)
container.center / container.paddingRedefine the utility with @utility container { … }
corePluginsUnsupported. Remove a whole namespace in @theme instead — --color-*: initial
safelistUnsupported. Use @source inline(…)
separatorUnsupported. The separator is :

The replacement for safelist is @source inline(), which brace-expands:

CSS
@import "tailwindcss";

@source inline("underline");
@source inline("{hover:,focus:,}underline");
@source inline("bg-red-{50,{100..900..100},950}");

@source not inline(…) does the opposite, preventing a class from being generated even when it appears in your source.

prefix deserves its own note because the syntax moved as well as the config:

HTML
<!-- v3 -->
<div class="tw-flex tw-bg-red-500 hover:tw-bg-red-600">…</div>

<!-- v4 — the prefix is a variant-like token at the start -->
<div class="tw:flex tw:bg-red-500 tw:hover:bg-red-600">…</div>

The theme() function#

Deprecated, still works

Every theme value is a real CSS variable in v4, so reach for the variable:

CSS
/* Deprecated */
.card {
  background-color: theme(colors.red.500);
  padding: theme(spacing.4);
}

/* v4 */
.card {
  background-color: var(--color-red-500);
  padding: --spacing(4);
}

theme() is still the answer inside media queries, where custom properties are not permitted — but the argument changed from dot notation to the variable's name:

CSS
/* v3 */
@media (width >= theme(screens.xl)) { … }

/* v4 */
@media (width >= theme(--breakpoint-xl)) { … }

Related removals in the JavaScript API:

RemovedReplacement
resolveConfig()Read the CSS variables — getComputedStyle(document.documentElement).getPropertyValue('--shadow-xl')
Importing tailwindcss/defaultTheme for valuesThe generated CSS variables, or @theme static to force them all to be emitted

The v3 Play CDN#

Wrong version

HTML
<!-- v3 Play CDN — does not understand any v4 syntax -->
<script src="https://cdn.tailwindcss.com"></script>

<!-- v4 -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

https://cdn.tailwindcss.com still serves Tailwind v3, so a page using it will render @tailwind directives and a tailwind.config object happily — and reject @theme, @utility and every v4 class rename. It is the fastest way to end up debugging a "v4 bug" that is actually v3.

Configuration also moved. v3's inline config object becomes a type="text/tailwindcss" style block:

HTML
<!-- v3 -->
<script>tailwind.config = { theme: { extend: { colors: { brand: '#4f46e5' } } } }</script>

<!-- v4 -->
<style type="text/tailwindcss">
  @theme { --color-brand: oklch(0.51 0.23 277); }
</style>

Neither CDN belongs in production — see Installation.

npx tailwindcss init#

Removed There is no config file to generate, so the command was removed along with -p (which also wrote a postcss.config.js).

BASH
npx tailwindcss init -p    # v3 only — the command no longer exists

There is no v4 equivalent to run. Create the CSS file yourself:

app.css
@import "tailwindcss";

A tutorial that opens with npx tailwindcss init was written for v3. Treat everything after it with suspicion.

The CLI binary moved as well: npx tailwindcss -i in.css -o out.css is the v3 CLI. In v4 it is npx @tailwindcss/cli -i in.css -o out.css.

darkMode: 'class'#

Removed There is no darkMode option, because there is no config. @custom-variant replaces it and covers cases the option never could.

Legacy — tailwind.config.js
module.exports = { darkMode: 'class' }
app.css
@custom-variant dark (&:where(.dark, .dark *));

For a data-theme attribute instead of a class:

CSS
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));

darkMode: 'media' was v3's default and needs no replacement — following the OS preference is v4's default too. Full detail in Dark Mode.

The @tailwind directives#

Removed

Legacy — v3
@tailwind base;
@tailwind components;
@tailwind utilities;
v4
@import "tailwindcss";

Unlike most entries on this page, this one fails loudly — the directives are not recognised and the build errors. That is the good case.

If your v3 setup deliberately omitted a layer, the pieces are available as separate imports:

CSS
@layer theme, base, components, utilities;

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);

Omitting @import "tailwindcss/preflight.css" layer(base); is how you opt out of Preflight.

Plugins replaced by core#

Uninstall these

PluginNow in core as
@tailwindcss/line-clampline-clamp-1 … line-clamp-none
@tailwindcss/aspect-ratioaspect-square, aspect-video, aspect-[4/3], and the --aspect-* theme namespace
@tailwindcss/container-queries@container, @sm: … @7xl:, @max-md:, and --container-*

Leaving them installed is not harmless: you end up with two definitions of the same class names competing in the cascade, and the plugin version usually wins by source order.

@tailwindcss/typography and @tailwindcss/forms are not deprecated. Load them with @plugin:

CSS
@plugin "@tailwindcss/typography";

@plugin "@tailwindcss/forms" {
  strategy: class;
}

How to find them in your codebase#

Run these from the project root. rg is ripgrep; the grep equivalents are given where the pattern is portable.

Removed classes — these produce no CSS, so every hit is a real bug:

BASH
rg -n '\b(bg|text|border|divide|ring|placeholder)-opacity-[0-9]+'
rg -n '\bflex-(grow|shrink)\b'
rg -n '\boverflow-ellipsis\b'
rg -n '\bdecoration-(slice|clone)\b'

Renamed size utilities — these still compile, so every hit needs a human decision:

BASH
rg --pcre2 -n '(?<=["\s])(shadow|drop-shadow|blur|backdrop-blur|rounded)(?=["\s])'
rg -n '\b(shadow|drop-shadow|blur|backdrop-blur|rounded)-sm\b'

The first pattern finds bare utilities whose meaning shifted one step down. The second finds the -sm steps, which in v4 mean what -xs means in the v3 scale.

The lookarounds need ripgrep's PCRE2 engine (--pcre2 or -P). Without it, grep -REn '(^|[" ])(shadow|rounded|blur)([" ]|$)' src/ gets close, with more false positives.

Outline, ring and gradients:

BASH
rg -n '\boutline-none\b'
rg --pcre2 -n '(?<=["\s])ring(?=["\s])'
rg -n '\bbg-gradient-to-'

Syntax that moved:

BASH
rg --pcre2 -n '(?<=["\s])!(?=[a-z])'   # !flex        -> flex!
rg -n '\[--[a-z]'                      # bg-[--brand] -> bg-(--brand)
rg -n '\[[a-z0-9%.]+,[a-z0-9%.]+\]'    # commas in arbitrary values -> underscores

Build setup and config:

BASH
rg -n 'cdn\.tailwindcss\.com'
rg -n '@tailwind\s+(base|components|utilities)'
rg -n 'tailwindcss init'
rg -n 'resolveConfig|tailwindcss/defaultTheme'
rg -n 'corePlugins|safelist|separator|darkMode' -g 'tailwind.config.*'
rg -n '@tailwindcss/(line-clamp|aspect-ratio|container-queries)' -g 'package.json'
rg -n 'theme\(' -g '*.css'

Dynamic class names, which used to need safelist:

BASH
rg -n '(bg|text|border)-\$\{'          # template literals
rg -n '`(bg|text|border)-'             # any interpolated class string

Common mistakes#

  • Assuming a green build means a clean migration. Most entries on this page still compile. shadow, rounded, blur, ring and outline-none are all valid v4 classes with different meanings.
  • Leaving focus:outline-none alone because it still compiles. The class name did not change; the behaviour did. If you are hiding an outline to draw your own ring, you want outline-hidden.
  • Keeping the container-queries or line-clamp plugin "just in case". Two definitions of the same utilities is worse than either one alone.
  • Trusting @config to carry a safelist. It is ignored in silence — the classes simply are not generated.
  • Reading theme() as removed. It is deprecated, not gone, and it is still correct inside media queries with the new theme(--breakpoint-xl) argument form.
  • Using https://cdn.tailwindcss.com to test a v4 snippet. That URL serves v3, so you are debugging the wrong version.