"htmx got us 90% there—then we hit a wall. Here’s how Alpine.js saved us without blowing up our stack."
htmx is brilliant for server-driven interactivity, but when you need client-side state, animations, or complex DOM manipulations, pure HTML attributes start feeling like straitjackets.
After rebuilding the same component three times in htmx before giving up, we learned the hard way where Alpine.js shines—and where it’s overkill.
1. The htmx Sweet Spot (Where It’s Enough)
✅ Form submissions with swaps
✅ Simple filters (e.g., /products?sort=price
)
✅ Live search (hx-trigger="keyup")
<!-- Perfect htmx use case -->
<input
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
/>
2. When to Reach for Alpine.js
Case 1: Client-Side State
Problem in htmx:
<!-- Toggle visibility? Prepare for attribute soup -->
<div
hx-get="/details"
hx-target="#details"
hx-swap="morphdom"
...5 more attributes...
>
Solution with Alpine:
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>Content</div>
</div>
Case 2: Animations
htmx Struggle:
<!-- Impossible without JS -->
<div hx-swap="innerHTML settle:100ms">
<!-- No enter/leave transitions -->
</div>
Alpine Save:
<div
x-data="{ show: false }"
x-init="setTimeout(() => show = true, 100)"
x-show="show"
x-transition:enter.duration.300ms
>
Smoothly animated content
</div>
Case 3: Complex Event Handling
htmx Limitation:
<!-- Can’t easily prevent default or debounce -->
<form hx-post="/submit" hx-trigger="submit">
Alpine Fix:
<form
x-data="{ submit: false }"
@submit.prevent="submit = true; $nextTick(() => $el.requestSubmit())"
hx-post="/submit"
hx-trigger="submit"
>
3. The Hybrid Architecture We Landed On
Layer Responsibilities
Tool | Role | Example |
---|---|---|
htmx | Server communication |
hx-get , hx-post
|
Alpine | Client-side logic |
x-data , x-show , @click
|
Stimulus | Legacy JS components | Complex drag/drop |
File Structure
app/
views/ # HTML with htmx/Alpine
javascript/
controllers/ # Stimulus (if needed)
components/ # Alpine x-data stores
4. Rules of Thumb
✅ Add Alpine when:
- You need client-side state (toggles, counters)
- Animations are required
- Events need throttling/debouncing
❌ Avoid Alpine when:
- htmx attributes alone suffice
- You’re not using transitions
- Team doesn’t know Alpine yet
5. Migration Tips
From Pure htmx to Hybrid
- Start with one component (e.g., dropdown)
-
Keep htmx for server calls (
hx-get
,hx-post
) -
Use Alpine for local state (
x-data
,x-show
)
<!-- Before (htmx-only) -->
<div hx-get="/notifications" hx-trigger="every 5s">
<%= render @notifications %>
</div>
<!-- After (htmx + Alpine) -->
<div
x-data="{ unread: 0 }"
hx-get="/notifications"
hx-trigger="every 5s"
hx-swap="innerHTML"
@htmx:after-swap="unread = countNewItems()"
>
<span x-text="unread" x-show="unread > 0"></span>
<%= render @notifications %>
</div>
"But We’re a React Shop!"
That’s okay. Try this:
- Use Alpine for one small feature (e.g., accordion)
- Compare dev experience to React
- Keep React for complex SPAs
Have you mixed htmx + Alpine? Share your wins (or regrets) below!
Top comments (0)