Type something to search...
How to set up Tailwind CSS in Astro?

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:

  1. Install the packages:

    npm install tailwindcss @tailwindcss/vite
    
  2. Add the Vite plugin to astro.config.mjs, exactly as shown above.

  3. Create the stylesheet at src/styles/global.css:

    /* src/styles/global.css */
    @import "tailwindcss";
    
  4. Import it in your layout or page with import "../styles/global.css";.

  5. Start the dev server with npm run dev and add a class such as text-3xl font-bold to 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}-500 can'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_modules or 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:

  1. Add Tailwind 4 with npx astro add tailwind, or install tailwindcss and @tailwindcss/vite manually.

  2. Remove the old integration:

    npm uninstall @astrojs/tailwind
    
  3. Delete it from your config: remove the import tailwind from "@astrojs/tailwind" line and tailwind() from integrations.

  4. Move your theme: convert theme.extend values into @theme variables in CSS, and replace @tailwind base; @tailwind components; @tailwind utilities; with @import "tailwindcss";.

  5. 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 locationintegrations: [tailwind()]vite: { plugins: [tailwindcss()] }
Themetailwind.config.mjs@theme in CSS
Content pathsManual content arrayAutomatic detection, @source for extras
Entry CSS@tailwind directives@import "tailwindcss";
Pluginsplugins: [] 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.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper