Tailwind v4 in Astro Without PostCSS Plugins

Tailwind 4 rewrote the engine and the configuration model. It is a Vite plugin rather than a PostCSS plugin, its configuration lives in CSS rather than tailwind.config.js, and it detects sources automatically. For Astro sites, that means the @astrojs/tailwind integration and the PostCSS pipeline it set up are no longer needed: Astro already runs Vite, and Tailwind plugs straight into it.

This guide migrates a 900-page Astro 5 documentation and marketing site from Tailwind 3.4 with @astrojs/tailwind to Tailwind 4 with @tailwindcss/vite, converts the JavaScript config to CSS, keeps dark mode working with the site's data-attribute toggle, and measures the output and build time before and after. It belongs to CSS and JavaScript Asset Optimization.

Prerequisites

  • Astro 5 (Astro 4.x also works with the Vite plugin).
  • A clean git tree and the current build's CSS output saved for comparison.
  • Browsers targeted by Tailwind 4: it relies on cascade layers, @property and color-mix(), which need Safari 16.4+, Chrome 111+ and Firefox 128+. Check analytics first; on this site, older browsers were 0.4% of visits.

Step 1: Swap the Integration for the Vite Plugin

npm uninstall @astrojs/tailwind autoprefixer postcss
npm install tailwindcss@4 @tailwindcss/vite@4
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: { plugins: [tailwindcss()] },
});

Remove postcss.config.cjs if Tailwind and autoprefixer were its only plugins — Tailwind 4 handles vendor prefixes through Lightning CSS internally.

Step 2: Import Tailwind in CSS

The integration had injected Tailwind's base styles automatically. Now a global stylesheet imports it, and the base layout imports that stylesheet:

/* src/styles/global.css */
@import 'tailwindcss';
---
import '../styles/global.css';
---
Tailwind 3 with the Astro integration compared with Tailwind 4 on the Vite plugin Two panels. Tailwind 3 used the astrojs tailwind integration, a PostCSS config with autoprefixer, a tailwind.config.js with content paths, theme and plugins, and injected base styles. Tailwind 4 uses the tailwindcss vite plugin in astro.config, a global CSS file importing tailwindcss, theme and sources declared in CSS, and automatic source detection. The two setups side by side Tailwind 3 + integration @astrojs/tailwind postcss.config.cjs tailwind.config.js content paths listed 3 config files Tailwind 4 + Vite plugin @tailwindcss/vite in astro.config global.css with @import @theme and @source in CSS automatic source detection 1 CSS file
Three config files became one CSS file and one line in the Astro config.

Step 3: Move the Config Into CSS

The old tailwind.config.js had content paths, a theme extension, one plugin and dark mode set to a class strategy. The official upgrade tool converts most of it:

npx @tailwindcss/upgrade

It rewrote templates for renamed utilities and produced a CSS config. After tidying, the result:

@import 'tailwindcss';
@plugin '@tailwindcss/typography';

@source '../content/external';
@source not '../data';

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

@theme {
  --font-sans: 'Inter Variable', system-ui, sans-serif;
  --color-brand-50: oklch(0.97 0.02 250);
  --color-brand-500: oklch(0.58 0.15 250);
  --color-brand-700: oklch(0.45 0.14 250);
  --breakpoint-3xl: 112rem;
}
  • @plugin loads the typography plugin, which works with Tailwind 4.
  • @source adds content outside automatic detection (a gitignored external content folder) and excludes data files that would otherwise produce accidental utilities, as covered in Keeping Tailwind CSS Output Small on Static Sites.
  • @custom-variant dark makes dark: utilities follow the site's data-theme attribute instead of the OS preference, matching its existing toggle.
  • @theme defines tokens as CSS variables, which also makes them available to plain CSS and inline SVG styles as var(--color-brand-500).

Step 4: Fix the Renamed and Changed Utilities

The upgrade tool handled most renames, but the visual diff of 40 key pages found four changes it could not:

Change in Tailwind 4Effect on this siteFix
Default border colour is currentColor (was gray-200)Bare border turned darkAdd border-gray-200 where needed, or set a base style
ring defaults to 1 px (was 3 px)Focus rings looked thinUse ring-3 explicitly
Placeholder colour uses current text colour at 50%Placeholders darkerSet placeholder colour in base
space-x/y selector changedSpacing off on items with hidden siblingsSwitch to gap with flex or grid

A base layer restored the old border and placeholder defaults for the whole site in five lines, which was quicker than editing hundreds of elements:

@layer base {
  *, ::before, ::after { border-color: var(--color-gray-200, currentColor); }
  input::placeholder, textarea::placeholder { color: var(--color-gray-400); }
}
Steps of the Tailwind 3 to Tailwind 4 migration in Astro Five steps. Swap the Astro integration for the Vite plugin. Import tailwindcss in a global stylesheet. Run the upgrade tool to move config into CSS and rename utilities. Compare 40 key pages visually and fix default changes for borders, rings and placeholders. Compare built CSS size and run the missing-class check. The migration in order Vite plugin replace integration @import in CSS global stylesheet Upgrade tool config to CSS Visual diff borders, rings, placeholders Size + class checks against old build
The whole migration took about half a day; most of it was the visual comparison.

Step 5: Verify Output and Classes

Two checks before merging. A visual comparison with Playwright screenshots of 40 pages in both themes caught the four default changes above. And the class check from Fixing Missing Tailwind Classes in Production Builds, which compares classes used in the built HTML with selectors in the built CSS, confirmed nothing was missing — automatic source detection had found everything the old content paths listed, plus the external folder once the @source was added.

Using Theme Tokens Outside Tailwind

A side benefit of the CSS-first theme is that every token is a real CSS custom property. Before the migration, the site duplicated its brand colours in three places: tailwind.config.js, a tokens.css file for hand-written component styles, and a JSON file read by the Open Graph image generator. Now the @theme block is the single source. Component styles use var(--color-brand-500) directly, scoped Astro styles do the same, and the OG image script reads the compiled CSS variables from the built stylesheet with a small regex. Inline SVG diagrams can reference the same variables, which made their dark-mode treatment simpler.

There is one caveat: tokens defined in @theme are emitted as variables whether utilities use them or not. Large palettes that exist only for documentation of the design system can be declared with @theme reference in a separate file imported where needed, so they are available to utilities without adding every variable to the shared stylesheet.

Monorepos and Shared Packages

The site lives in a monorepo with a shared UI package. Under Tailwind 3 the package had its own tailwind.config.js preset, imported by each app. In Tailwind 4 the package exports a CSS file with its @theme and @source directives, and each app's global stylesheet imports it after tailwindcss. Sources declared in the package's CSS are resolved relative to that file, so the package can point Tailwind at its own components without each app knowing its folder layout. That removed a class of bug where an app's content paths forgot a newly added package folder.

Measured Impact

MeasureTailwind 3.4 + integrationTailwind 4 + Vite pluginTool
CSS step in production build3.8 s0.4 sBuild log timings
Full production build61 s57 shyperfine, 5 runs
Dev: class change to browser~300 ms~40 msBrowser performance panel
CSS output, brotli12.4 KB11.2 KBbrotli -c | wc -c
Config files31 CSS fileRepository
Dependencies removed—3 (integration, postcss, autoprefixer)package.json
CSS build step, full build and dev update time before and after Paired bars. The CSS step in the production build went from 3,800 to 400 milliseconds. A class change reaching the browser in development went from 300 to 40 milliseconds. Total production build went from 61 to 57 seconds. Build and dev server timing Tailwind 3.4 Tailwind 4 CSS build step 3800 ms 400 ms Dev class change 300 ms 40 ms
Production timings from build logs and hyperfine; dev timing from the browser performance panel, median of ten edits.

The full build improved by only 4 seconds because CSS generation was a small part of it; the dev server change was what developers noticed.

Pitfalls & Rollback

  • Keeping the old integration alongside the Vite plugin. Remove it; running both generates CSS twice.
  • Skipping the visual diff. Default border, ring and placeholder changes are not errors.
  • Forgetting the dark variant. Without @custom-variant, dark: follows the OS, not the toggle.
  • Dropping support for older browsers unknowingly. Check analytics against Tailwind 4's browser targets.
  • Rollback: the migration is one branch; reverting it restores the integration, PostCSS config and JavaScript config together.

Conclusion

Tailwind 4 in Astro is simpler than Tailwind 3 was: one Vite plugin, one CSS file holding the theme, sources and variants, and no PostCSS. The migration took half a day on a 900-page site, most of it spent on a visual comparison that caught four changed defaults, and it made the CSS step nine times faster and class edits in the dev server near-instant.

FAQ

Do I still need the @astrojs/tailwind integration with Tailwind 4?

No. Tailwind 4 ships a Vite plugin, @tailwindcss/vite, which Astro can use directly through its vite.plugins setting. The older Astro integration was built around Tailwind 3 and PostCSS. Removing it and adding the Vite plugin is the recommended setup for Tailwind 4 in Astro.

Where does tailwind.config.js go in Tailwind 4?

Configuration moves into CSS. Theme values are defined with the theme directive as CSS variables, sources with the source directive, plugins with the plugin directive and custom variants with the custom-variant directive. A JavaScript config can still be loaded with the config directive during a migration, but the CSS-first form is simpler.

How is dark mode configured in Tailwind 4?

By default dark variants follow prefers-color-scheme. For a toggle that sets a class or data attribute on the html element, define a custom variant in CSS, for example custom-variant dark with a selector for data-theme dark, so dark utilities apply when that attribute is present.

Is Tailwind 4 faster than Tailwind 3 in Astro builds?

In this guide, the CSS step of the production build went from about 3.8 seconds to 0.4 seconds, and the dev server reflected class changes in about 40 milliseconds instead of 300. The overall build time fell by about 4 seconds on a 900-page site, because CSS generation was only a small part of it.

Filed underHow-tosAstro