DEV Community

Alex Aslam
Alex Aslam

Posted on

When to Add Alpine.js to htmx

"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"
/>
Enter fullscreen mode Exit fullscreen mode

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...
>
Enter fullscreen mode Exit fullscreen mode

Solution with Alpine:

<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open" x-transition>Content</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Case 2: Animations

htmx Struggle:

<!-- Impossible without JS -->
<div hx-swap="innerHTML settle:100ms">
  <!-- No enter/leave transitions -->
</div>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Case 3: Complex Event Handling

htmx Limitation:

<!-- Can’t easily prevent default or debounce -->
<form hx-post="/submit" hx-trigger="submit">
Enter fullscreen mode Exit fullscreen mode

Alpine Fix:

<form
  x-data="{ submit: false }"
  @submit.prevent="submit = true; $nextTick(() => $el.requestSubmit())"
  hx-post="/submit"
  hx-trigger="submit"
>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Start with one component (e.g., dropdown)
  2. Keep htmx for server calls (hx-get, hx-post)
  3. 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>
Enter fullscreen mode Exit fullscreen mode

"But We’re a React Shop!"

That’s okay. Try this:

  1. Use Alpine for one small feature (e.g., accordion)
  2. Compare dev experience to React
  3. Keep React for complex SPAs

Have you mixed htmx + Alpine? Share your wins (or regrets) below!

Top comments (0)