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
- ⚡ Ultra-lightweight (≈10KB gzipped)
- 🧠 Simple mental model (similar to Vue directives)
- 🚀 No hydration required
- 💡 Declarative syntax — easy to read and maintain
When Not to Use Alpine
If your app has:
- Deeply nested shared state
- Dynamic rendering from APIs
- Real-time collaboration or streaming content
…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.