Skip to content
Astro as patterns 4 min read

Tailwind CSS

Tailwind CSS lets you style components by composing small utility classes directly in your markup, which pairs naturally with Astro’s component-first model. Because Astro ships zero JavaScript by default, Tailwind’s purely build-time, CSS-only output is an ideal match — the styling adds nothing to the client bundle. As of Tailwind v4, the integration runs through a first-class Vite plugin, so there is no PostCSS config and almost no setup. This page covers wiring up the plugin, applying utilities across .astro and framework components, and keeping the workflow fast.

Installing the Vite plugin

Install Tailwind and its official Vite plugin, then create a single CSS entry point that imports Tailwind. In v4, configuration lives in CSS rather than a JavaScript config file, so a one-line @import is all you need to start.

npm install tailwindcss @tailwindcss/vite

Create a global stylesheet that pulls in the framework:

/* src/styles/global.css */
@import "tailwindcss";

Register the plugin with Vite inside your Astro config. Astro exposes the vite key precisely so integrations like this can hook into the underlying build.

// astro.config.mjs
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";

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

Finally, import the stylesheet once in a shared layout so every page that uses the layout gets the generated utilities.

---
// src/layouts/BaseLayout.astro
import "../styles/global.css";
const { title } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>{title}</title>
  </head>
  <body class="bg-slate-50 text-slate-900 antialiased">
    <slot />
  </body>
</html>

Importing global.css in one layout is enough — Vite bundles, dedupes, and tree-shakes the CSS. Re-importing it in every component is harmless but unnecessary.

Applying utilities in components

With the plugin active, write utility classes on any element. Tailwind scans your source files for class names at build time and emits only the CSS you actually use, so the output stays tiny regardless of how many utilities the framework offers.

---
// src/components/Card.astro
interface Props {
  title: string;
  href: string;
}
const { title, href } = Astro.props;
---
<a
  href={href}
  class="block rounded-xl border border-slate-200 bg-white p-6 shadow-sm transition hover:border-slate-300 hover:shadow-md"
>
  <h3 class="text-lg font-semibold tracking-tight">{title}</h3>
  <p class="mt-2 text-sm text-slate-500">Read the guide &rarr;</p>
</a>

Astro’s class:list directive composes well with Tailwind when you need conditional classes — pass strings, arrays, or objects and Astro flattens them into a single class attribute:

---
const { active = false } = Astro.props;
---
<button
  class:list={[
    "rounded-md px-4 py-2 text-sm font-medium",
    { "bg-blue-600 text-white": active, "bg-slate-100 text-slate-700": !active },
  ]}
>
  <slot />
</button>

The same classes work inside hydrated framework islands. Because Tailwind output is global CSS, a React or Svelte component using client:load is styled by the same utility layer with no extra configuration:

---
import Counter from "../components/Counter.tsx";
---
<Counter client:load />
// src/components/Counter.tsx
import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button
      onClick={() => setCount((c) => c + 1)}
      class="rounded bg-emerald-600 px-3 py-1 font-semibold text-white"
    >
      Count: {count}
    </button>
  );
}

Customizing the theme

Tailwind v4 reads design tokens from the @theme directive in your CSS. Define custom colors, fonts, or spacing there and they become available as utilities and as CSS variables automatically.

/* src/styles/global.css */
@import "tailwindcss";

@theme {
  --color-brand: #4f46e5;
  --font-display: "Inter", sans-serif;
}

You can now use bg-brand, text-brand, or font-display anywhere. The tokens are also exposed as var(--color-brand), which lets Astro <style> blocks and scoped styles share the same palette.

Concernv4 (Vite plugin)v3 (legacy)
Setup@tailwindcss/vite plugin@astrojs/tailwind + PostCSS
ConfigCSS @theme directivetailwind.config.js
Content pathsAuto-detectedDeclared in content array
Import@import "tailwindcss"@tailwind directives

The older @astrojs/tailwind integration targets Tailwind v3 and is now deprecated. For new projects use @tailwindcss/vite with Tailwind v4 as shown above.

Best practices

  • Import your single global.css once in a base layout rather than per component to avoid duplicate @import work.
  • Define brand colors, fonts, and spacing in @theme so utilities and scoped <style> blocks share one source of truth.
  • Prefer composing utilities directly in markup; reach for @apply only to dedupe genuinely repeated class strings.
  • Keep utility classes in Astro’s class:list directive for conditional logic instead of string concatenation.
  • Let Tailwind purge unused styles automatically — never hand-build long, dynamically concatenated class names it cannot detect.
  • Co-locate islands and static components; the shared global stylesheet styles both with zero extra setup.
Last updated June 14, 2026
Was this helpful?