← Back

Handling Client-Side State in Astro with Alpine.js — No React Required

Kavi Castelo · August 24, 2025

Introduction

Astro encourages shipping less JavaScript. But what if your static site needs interactivity — dropdowns, tabs, toggles, modals?

While you can hydrate React or Vue components in Astro, there’s a simpler solution: Alpine.js.

It gives you reactive, declarative state management — directly in your HTML. No build config, no bundling, no state library required.

Let’s walk through how I’ve used Alpine.js in my Astro projects.


1. Setting Up Alpine.js in Astro

Just include the script in your layout file:

<!-- src/layouts/BaseLayout.astro -->
<head>
  <script src="https://cdn.jsdelivr.net/npm/alpinejs" defer></script>
</head>

Now Alpine is globally available across your .astro pages.


2. A Basic Toggle Example

Let’s build a simple FAQ toggle

---
// src/components/FaqItem.astro
const { question, answer } = Astro.props;
---

<div x-data="{ open: false }" class="border border-gray-700 rounded-lg">
  <button @click="open = !open" class="w-full text-left p-4 bg-gray-800 hover:bg-gray-700 flex justify-between items-center">
    <span>{question}</span>
    <i class="fa" :class="open ? 'fa-minus' : 'fa-plus'"></i>
  </button>
  <div x-show="open" x-transition class="p-4 bg-gray-700 text-gray-300 prose prose-invert">
    {answer}
  </div>
</div>

✅ No hydration needed. Just works. ⚡


3. Tabs with Alpine.js

Let’s create a lightweight tab component.

<div x-data="{ tab: 'html' }" class="space-y-2">
  <div class="flex space-x-4">
    <button :class="tab === 'html' ? 'text-blue-400' : 'text-gray-400'" @click="tab = 'html'">HTML</button>
    <button :class="tab === 'css' ? 'text-blue-400' : 'text-gray-400'" @click="tab = 'css'">CSS</button>
    <button :class="tab === 'js' ? 'text-blue-400' : 'text-gray-400'" @click="tab = 'js'">JS</button>
  </div>

  <div x-show="tab === 'html'">🧱 HTML content goes here.</div>
  <div x-show="tab === 'css'">🎨 CSS content goes here.</div>
  <div x-show="tab === 'js'">⚙️ JS content goes here.</div>
</div>

This gives you fully functional tabs — no React state, no context provider, no bundle size bloat.


4. Modal Component with Escape Key Close

<div x-data="{ show: false }">
  <button @click="show = true">Open Modal</button>

  <div
    x-show="show"
    @keydown.escape.window="show = false"
    class="fixed inset-0 bg-black/70 flex justify-center items-center z-50"
  >
    <div class="bg-white p-6 rounded shadow text-black">
      <h2 class="text-lg font-bold">Modal Title</h2>
      <p>This is a lightweight modal.</p>
      <button @click="show = false" class="mt-4 text-blue-500">Close</button>
    </div>
  </div>
</div>

✨ Bonus: The modal closes when you hit Esc.


Why I Prefer Alpine.js in Astro


When Not to Use Alpine

If your app has:

…you’ll want a framework like React or Solid.

But for portfolios, blogs, or static sites with sprinkles of interactivity — Alpine is perfect.


Conclusion

Alpine.js lets you build interactive components in Astro without a full framework. It’s perfect for toggles, dropdowns, tabs, modals, and more.

I’ve used it to reduce bundle size, simplify markup, and speed up page loads — all while keeping full control of interactivity.

If you’re building with Astro and want fast, clean, minimal JS — give Alpine a try.