Skip to content
Development AstroTypeScript

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.

Luciano Ferreira Luciano Ferreira
3 min read
Ler em Português
Astro and Tailwind CSS v4 - Performant sites

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.