
How to set up Tailwind CSS in Astro?
To set up Tailwind CSS in Astro, run npx astro add tailwind. The command installs tailwindcss and the official @tailwindcss/vite plugin, adds the plugin to vite.plugins in astro.config.mjs, and creates src/styles/global.css containing @import "tailwindcss";. You then import that CSS file in your base layout, and every utility class you use in .astro, Markdown or framework component files is generated automatically. There's no tailwind.config.js and no content array to maintain with Tailwind 4.
This is a change from older tutorials, which used the @astrojs/tailwind integration. That integration is only for legacy Tailwind 3 projects; Tailwind 4 plugs into Vite directly. In this article you'll learn the one-command and manual setups, how to customise your theme in CSS, how to use utilities together with class:list and component props, how to use @apply in scoped styles (which needs one extra line), how to add dark mode and the typography plugin, and how to migrate an existing Tailwind 3 project. The examples were tested with Astro 7.4.2 and Tailwind 4.3.3.
Quick Setup with astro add
From your project root:
npx astro add tailwind
Astro shows you the config change and asks for confirmation (add --yes to skip the prompts). The resulting config is short:
// astro.config.mjs
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});
Note where the plugin goes: in vite.plugins, not in Astro's integrations array. Tailwind 4 is a Vite plugin, and Astro runs on Vite.
The command also tells you the one step it can't do for you:
action required You must import your Tailwind stylesheet, e.g. in a shared layout:
So import the stylesheet in your base layout:
---
// src/layouts/BaseLayout.astro
import "../styles/global.css";
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="bg-white text-slate-900 antialiased">
<slot />
</body>
</html>
Every page that uses this layout can now use Tailwind classes. The pattern of importing site-wide CSS in a layout is covered in more depth in the post on global styles in Astro.
Manual Setup
If you prefer to do it by hand, or astro add can't edit an unusual config file, the steps are:
-
Install the packages:
npm install tailwindcss @tailwindcss/vite -
Add the Vite plugin to
astro.config.mjs, exactly as shown above. -
Create the stylesheet at
src/styles/global.css:/* src/styles/global.css */ @import "tailwindcss"; -
Import it in your layout or page with
import "../styles/global.css";. -
Start the dev server with
npm run devand add a class such astext-3xl font-boldto check it works.
How Tailwind Finds Your Classes
Tailwind 4 detects source files automatically. It scans your project for class names, skipping files listed in .gitignore, node_modules, binary files, CSS files and lock files, so .astro, .md, .mdx, .tsx, .vue and .svelte files are all picked up without configuration.
Two rules still apply:
- Write complete class names: Tailwind reads files as plain text.
bg-${color}-500can't be detected; map values to full strings instead, such as{ red: "bg-red-500", blue: "bg-blue-500" }. - Register sources outside the project: if classes live in a package in
node_modulesor a sibling folder in a monorepo, add it with@source "../path";in your CSS file.
Customising the Theme in CSS
Tailwind 4 moves theme configuration into CSS with the @theme directive. Each variable you define creates matching utilities:
/* src/styles/global.css */
@import "tailwindcss";
@theme {
--color-brand-500: oklch(0.62 0.19 255);
--color-brand-700: oklch(0.48 0.18 255);
--font-display: "Georgia", serif;
}
Now bg-brand-500, text-brand-700, hover:bg-brand-500/10 and font-display all work. The variables are also emitted as real CSS custom properties, so you can use var(--color-brand-500) in any scoped style block.
If you load fonts with Astro's Fonts API, map its CSS variable into the theme with @theme inline, as shown in the post on custom fonts in Astro:
@theme inline {
--font-sans: var(--font-inter);
}
Using Tailwind in Astro Components
Utilities go straight into class attributes. For classes that depend on props, class:list is the cleanest tool, because it skips falsy values:
---
// src/components/Button.astro
interface Props {
variant?: "primary" | "ghost";
class?: string;
}
const { variant = "primary", class: className } = Astro.props;
---
<button
class:list={[
"rounded-md px-4 py-2 text-sm font-medium",
variant === "primary" && "bg-brand-500 text-white hover:bg-brand-700",
variant === "ghost" && "text-brand-700 hover:bg-brand-500/10",
className,
]}
>
<slot />
</button>
Used as <Button variant="ghost" class="ml-2">Cancel</Button>, this rendered:
<button class="rounded-md px-4 py-2 text-sm font-medium text-brand-700 hover:bg-brand-500/10 ml-2">
Because Tailwind classes are global, the parent's ml-2 works without the ...rest spread that scoped styles need. More patterns are in the post on class:list in Astro.
One caveat: if a parent passes a class that conflicts with a default, such as px-6 against px-4, both end up on the element and the winner is decided by the order of rules in Tailwind's CSS, not the order in the attribute. Avoid conflicting defaults, expose a prop for the variation, or use a helper such as the tailwind-merge package to resolve conflicts.
Using @apply in Scoped Styles
You can still use @apply inside an Astro <style> block, but each block is processed separately, so Tailwind doesn't know your theme or utilities there. Without extra setup, the build fails:
Cannot apply unknown utility class `bg-brand-500`. Are you using CSS modules or similar and missing `@reference`?
The fix is to reference your main stylesheet at the top of the block:
---
// src/components/Badge.astro
---
<span class="badge"><slot /></span>
<style>
@reference "../styles/global.css";
.badge {
@apply rounded-full bg-brand-500 px-2 py-0.5 text-xs text-white;
}
</style>
@reference imports the theme and utilities for lookup only, so no CSS is duplicated. The output is a normal scoped rule, .badge[data-astro-cid-...], with the utility declarations expanded into it. Use @apply sparingly; utilities in markup are usually easier to read and keep the CSS bundle small.
Adding Dark Mode
By default Tailwind's dark: variant follows the operating system through prefers-color-scheme. If you want a toggle, redefine the variant to follow a class on the root element:
/* src/styles/global.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
Then dark:bg-slate-950 compiles to .dark\:bg-slate-950:where(.dark,.dark *), which applies whenever an ancestor has the dark class:
<body class="bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<slot />
</body>
Adding and persisting that class without a flash of the wrong theme takes a small inline script; the full approach is in the post on dark mode in Astro.
Styling Markdown with the Typography Plugin
Rendered Markdown has no classes, so utilities can't reach it directly. The official typography plugin adds prose classes that style headings, lists, code and tables in one go:
npm install -D @tailwindcss/typography
In Tailwind 4, plugins are loaded from CSS with @plugin:
/* src/styles/global.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
Then wrap your content:
---
// src/layouts/PostLayout.astro
import BaseLayout from "./BaseLayout.astro";
const { frontmatter } = Astro.props;
---
<BaseLayout title={frontmatter.title}>
<article class="prose prose-slate mx-auto dark:prose-invert">
<h1>{frontmatter.title}</h1>
<slot />
</article>
</BaseLayout>
The same layout works for content collections rendered with render(entry) and <Content />, which is the setup used in the post on MDX blog posts in Astro.
Migrating from Tailwind 3 and @astrojs/tailwind
If your project was set up before Tailwind 4, it probably uses the @astrojs/tailwind integration and a tailwind.config.mjs file. To upgrade:
-
Add Tailwind 4 with
npx astro add tailwind, or installtailwindcssand@tailwindcss/vitemanually. -
Remove the old integration:
npm uninstall @astrojs/tailwind -
Delete it from your config: remove the
import tailwind from "@astrojs/tailwind"line andtailwind()fromintegrations. -
Move your theme: convert
theme.extendvalues into@themevariables in CSS, and replace@tailwind base; @tailwind components; @tailwind utilities;with@import "tailwindcss";. -
Follow Tailwind's v4 upgrade guide for renamed utilities. Tailwind also provides an upgrade tool,
npx @tailwindcss/upgrade, which automates much of this.
If you need to stay on Tailwind 3 for now, the legacy route still works: install tailwindcss@3 and @astrojs/tailwind, add tailwind() to integrations, and keep a tailwind.config.mjs with a content array.
Tailwind 3 vs Tailwind 4 in Astro
| Tailwind 3 (legacy) | Tailwind 4 | |
|---|---|---|
| Package | @astrojs/tailwind integration | @tailwindcss/vite Vite plugin |
| Config location | integrations: [tailwind()] | vite: { plugins: [tailwindcss()] } |
| Theme | tailwind.config.mjs | @theme in CSS |
| Content paths | Manual content array | Automatic detection, @source for extras |
| Entry CSS | @tailwind directives | @import "tailwindcss"; |
| Plugins | plugins: [] in config | @plugin "..." in CSS |
FAQ: Tailwind CSS in Astro
No, not for Tailwind 4. The @astrojs/tailwind integration is only for legacy Tailwind 3 projects. Tailwind 4 uses the @tailwindcss/vite plugin added to vite.plugins in your Astro config.
No. Tailwind 4 is configured in CSS with directives such as @theme, @plugin, @source and @custom-variant. Source files are detected automatically, so there's no content array.
Each style block is processed on its own, so it doesn't know your theme. Add @reference followed by the path to your main stylesheet at the top of the style block, and the utilities become available.
Tailwind reads files as plain text, so classes built with string interpolation can't be detected. Use complete class names, and add folders outside the project with the @source directive.
Yes. Class names in framework component files are detected the same way, and the styles come from the same global stylesheet imported in your layout.
Yes. Utilities handle most styling in markup, while scoped style blocks are still useful for complex selectors and animations. Use @reference if you want to use @apply or theme functions inside them.
Conclusion
Setting up Tailwind CSS in Astro is now a single command: npx astro add tailwind adds the @tailwindcss/vite plugin and a stylesheet with @import "tailwindcss";, and all that's left is importing that file in your layout. Theme customisation, plugins and dark mode variants live in the same CSS file, and class detection works across Astro, Markdown and framework components without a config file.
The two details worth remembering are @reference for @apply in scoped styles and complete, non-interpolated class names. If you're upgrading an older project, swap @astrojs/tailwind for the Vite plugin and move your theme into @theme, and the rest of your markup will usually keep working.


