Tailwind CSS v4 ships a fundamentally different architecture from v3. The JavaScript config file is gone, the PostCSS plugin changed, the import syntax changed, and a handful of utility class names shifted just enough to break things silently. If you hit those changes all at once without a map, the upgrade can feel much harder than it actually is.
This guide walks through the full upgrade for an Astro project — from removing the old integration to handling the Astro-specific @reference requirement that catches almost everyone the first time. It assumes you are on Astro 5 and Tailwind v3, and want to land on Tailwind v4 with @tailwindcss/vite.
What actually changed between v3 and v4
Before touching a single file, it helps to understand the three big architectural shifts:
- CSS-first configuration.
tailwind.config.jsis no longer needed. Theme tokens, custom utilities, and variants move into your CSS file using the@themedirective. You can still reference a JS config file as an escape hatch, but the goal is to get everything into CSS. - New engine and new packages. The core engine is rewritten (partly in Rust via Lightning CSS). The PostCSS plugin moved from the
tailwindcsspackage to@tailwindcss/postcss. There is also a first-party Vite plugin at@tailwindcss/vite— and for Astro, the Vite plugin is the right choice. - Automatic content detection. The
contentarray in your config is gone. Tailwind v4 scans your project files automatically, respecting.gitignore. You only need to opt in to extra locations with@source.
Performance improved dramatically as a result: the Tailwind team reports full builds around 3-4x faster and incremental builds measured in microseconds rather than milliseconds.
Step 1 — Upgrade Astro to v5 first
Tailwind v4 requires Astro 5.2 or later. If you are already there, skip ahead. If not, run the official Astro upgrade tool and verify your build still passes before touching Tailwind:
npx @astrojs/upgrade
npm run build
Fix any Astro-level errors before proceeding. Mixing two migration problems at once makes both harder to debug.
Step 2 — Run the automated Tailwind upgrade tool
The Tailwind team ships an upgrade codemod that handles the majority of the mechanical work. Run it from the root of your project:
npx @tailwindcss/upgrade
If your working tree has uncommitted changes, the tool will refuse to run to protect you from losing anything. Either commit first or pass --force:
npx @tailwindcss/upgrade --force
The codemod will:
- Update
package.jsondependencies (removestailwindcss, adds@tailwindcss/viteand the newtailwindcssv4 package) - Rewrite your CSS entry point — replacing the three
@tailwinddirectives with@import "tailwindcss" - Attempt to migrate
tailwind.config.jstheme values into@themeblocks in CSS - Update renamed utility classes in HTML/Astro/JSX files (shadow, blur, rounded, ring)
It does not always get everything right, especially with complex configs or custom plugins. Treat it as a first pass, not a complete migration.
Step 3 — Remove the old Astro Tailwind integration
The @astrojs/tailwind package is deprecated for v4. Remove it from your project and from astro.config.mjs:
npm remove @astrojs/tailwind
Open astro.config.mjs and delete the import and the integration entry:
// BEFORE (v3)
import tailwind from "@astrojs/tailwind";
export default defineConfig({
integrations: [tailwind()],
});
// AFTER (v4) — tailwind moves to vite.plugins
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});
Note the critical difference: the Tailwind Vite plugin goes under vite.plugins, not integrations. Putting it in integrations is the most common mistake and it silently does nothing.
You may also need to delete postcss.config.cjs or postcss.config.mjs if the codemod left it behind. When using the Vite plugin, PostCSS config is not needed and can conflict.
Step 4 — Update your CSS entry point
Your CSS entry point — typically src/styles/global.css or similar — needs the old three-directive block replaced. The codemod usually handles this, but verify it looks like:
/* v3 (delete these) */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* v4 (should look like this) */
@import "tailwindcss";
If you had custom utilities registered with @layer utilities, those need to move to @utility blocks:
/* v3 */
@layer utilities {
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
}
/* v4 */
@utility visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
Step 5 — Migrate tailwind.config.js theme values to CSS
If your v3 config had custom colors, fonts, or spacing, those move into a @theme block in your CSS. The variable naming convention follows a predictable pattern: --color-*, --font-*, --spacing-*, etc.
/* v3 tailwind.config.js equivalent */
@import "tailwindcss";
@theme {
--color-brand: #2563eb;
--color-brand-light: #93c5fd;
--font-display: "Satoshi", sans-serif;
--breakpoint-xs: 30rem;
}
Every token you define in @theme is automatically exposed as a CSS custom property on :root, which means you can also use them in inline styles or non-Tailwind CSS: color: var(--color-brand).
If you have a complex v3 config with custom plugins that the codemod could not convert, you can keep the JS config as a temporary bridge:
@import "tailwindcss";
@config "./tailwind.config.js";
This lets you ship v4 without completing the full migration at once. Plan to move everything to CSS over time — the JS bridge is not intended as a permanent state.
Step 6 — Fix renamed utility classes
Several utility names shifted in v4. The codemod catches most of these, but run a search across your templates to verify. The most impactful renames:
shadow-smin v3 is nowshadow-xs;shadow(the default) is nowshadow-smblur-smin v3 is nowblur-xs;bluris nowblur-smrounded-smin v3 is nowrounded-xs;roundedis nowrounded-smring(3px blue) in v3 is nowring-3(and ring color defaults to currentColor, not blue)outline-noneis nowoutline-hidden
The important modifier syntax also changed. In v3, ! prefixed a class. In v4, it is a suffix:
<!-- v3 -->
<div class="!flex !text-red-500"></div>
<!-- v4 -->
<div class="flex! text-red-500!"></div>
And arbitrary CSS variable values changed from bracket syntax to parenthesis syntax:
<!-- v3 -->
<div class="bg-[--brand-color]"></div>
<!-- v4 -->
<div class="bg-(--brand-color)"></div>
Step 7 — The Astro-specific @reference gotcha
This is the one that trips up almost every Astro developer upgrading to v4. In v3, if you used @apply inside a <style> block in an .astro component, it just worked. In v4, it breaks.
The reason: Tailwind v4 uses CSS layers and scoped compilation. A <style> block in an Astro component is treated as a separate stylesheet that does not have access to your main Tailwind setup. The @apply text-red-500 call fails because v4 cannot find the utility definition.
The fix is to add a @reference directive at the top of any component style block that uses @apply:
---
// YourComponent.astro
---
<h1 class="heading">Hello</h1>
<style>
@reference "../../styles/global.css";
.heading {
@apply text-3xl font-bold text-brand;
}
</style>
The path should point to whatever CSS file has your @import "tailwindcss" line. @reference does not duplicate CSS output — it just tells the compiler where to look for utility and theme definitions. You can also reference Tailwind directly if you have no custom theme:
<style>
@reference "tailwindcss";
.heading {
@apply text-3xl font-bold;
}
</style>
If you have many components with @apply, manually adding @reference to each is tedious. The community Vite plugin astro-tw-autoreference injects it automatically. Alternatively, the more sustainable direction is to move away from @apply in component style blocks entirely — using Tailwind utility classes directly in your HTML or using CSS custom properties from @theme in plain CSS.
Step 8 — Verify the build
Run a full build and check for errors:
npm run build
Then spin up dev mode and do a visual walkthrough:
npm run dev
Things to specifically check:
- Shadows and borders — most commonly affected by the rename changes
- Focus ring styles — v4's ring default is 1px currentColor, not 3px blue
- Any component that uses
@applyin a<style>block - Custom theme colors — confirm they are resolving from
@theme - Mobile breakpoints — verify any custom breakpoints from your old config carried over
A note on browser support
Tailwind v4 requires browsers that support cascade layers, color-mix(), and registered custom properties. Practically, that means Safari 16.4+, Chrome 111+, and Firefox 128+. If your project must support older browsers — particularly older Safari — stay on Tailwind v3.4, which continues to receive bug fixes.
For most local business websites, the browser coverage is fine. Users on actively updated phones and laptops are well within the support window.
Wrapping up
The upgrade is more involved than a typical minor version bump, but the automated codemod handles the heaviest mechanical work. The changes that require manual attention are predictable: the Vite plugin wiring in astro.config.mjs, moving theme tokens from JS to CSS with @theme, double-checking the renamed shadow and ring utilities, and adding @reference to any component style block that uses @apply. Handle those four things carefully and the rest of the upgrade is largely mechanical.
The payoff is faster build times, a simpler setup with no tailwind.config.js to maintain, and design tokens that are real CSS custom properties available anywhere in your codebase — not just in Tailwind utility classes.