Building Performant Sites with Astro and Tailwind CSS v4
Learn how to combine Astro with Tailwind CSS v4 to create ultra-fast static sites, with design tokens via @theme and zero unnecessary JavaScript.
Why Astro + Tailwind CSS v4
Astro was built with a simple premise: ship less JavaScript. Combined with Tailwind CSS v4, which ditches the JS config file in favor of native CSS via @theme, you get a stack that results in Lighthouse 100 sites with no effort.
The portfolio you’re reading right now was built with this combination.
Setting up the project
The setup with Astro 5 and Tailwind v4 is straightforward:
npm create astro@latest my-site
cd my-site
npm install tailwindcss @tailwindcss/vite
In astro.config.mjs, add the Vite plugin:
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});
Design tokens with @theme
The big news in Tailwind v4 is defining tokens directly in CSS:
@import "tailwindcss";
@theme {
--color-brand: #0078D4;
--color-brand-hover: #006CBE;
--color-surface-primary: #FFFFFF;
--color-foreground-primary: #1A1D26;
--font-sans: "Inter", system-ui, sans-serif;
}
These tokens automatically become utility classes: bg-brand, text-foreground-primary, font-sans. No tailwind.config.js needed.
Dark mode with CSS custom properties
Dark mode works by overriding custom properties:
html.dark {
--color-brand: #4BA0E8;
--color-surface-primary: #0a0a0b;
--color-foreground-primary: #f0f0f0;
}
With a JavaScript toggle that adds/removes the dark class on <html> and saves to localStorage, you get full dark mode without any state management framework.
Astro components: zero JS by default
.astro components are rendered at build time and ship pure HTML. Use <script> only when you need interactivity:
---
const { title } = Astro.props;
---
<section>
<h2>{title}</h2>
<slot />
</section>
No JavaScript is sent to the client for this component. Compare that to React, where even a static <div> loads the runtime.
Reusable utility classes
Create classes in @layer components for repeated patterns:
@layer components {
.btn-primary {
@apply inline-flex items-center gap-2 rounded-full
bg-brand px-7 py-3.5 text-sm font-semibold
text-white transition-all hover:bg-brand-hover;
}
.card {
@apply rounded-2xl border border-stroke-subtle
bg-surface-primary p-6 transition-all
hover:border-brand/20 hover:shadow-xl;
}
}
Content Collections for the blog
Astro offers Content Collections with Zod schemas for typed posts:
const blog = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
lang: z.enum(["pt", "en"]).default("pt"),
}),
});
Every Markdown post is validated at build time. Type errors are caught before deploy.
Real performance
This portfolio, with 20+ pages, 16 blog posts, bilingual i18n, and dark mode:
- Build time: ~1 second
- JavaScript shipped: Only for interactions (theme toggle, filters, mobile menu)
- Lighthouse: 100/100 in Performance, Accessibility, Best Practices, and SEO
- First Contentful Paint: < 0.5s
When NOT to use Astro
Astro isn’t the answer to everything. If you need:
- Complex stateful apps (dashboards with heavy interactivity): Use Next.js or Remix
- Real-time apps (chat, collaboration): Use Next.js with WebSockets
- SPA with rich transitions: Use React Router or similar
Astro shines for content-oriented sites: portfolios, blogs, documentation, landing pages, and institutional websites.
Conclusion
The Astro + Tailwind CSS v4 combination delivers the best possible experience for static sites in 2026. Native CSS for design tokens, zero JavaScript by default, and typed Content Collections make this stack the ideal choice for developers who value performance and DX.