· alpinejs / htmx / javascript
Alpine.js vs HTMX — JS sprinkles or server fragments?
Alpine.js owns client-side UI state; HTMX owns server-driven HTML swaps. Use both together on any SSR stack — or pick one when scope is narrow.
By Ethan
1,333 words · 7 min read
Alpine.js and HTMX solve different problems. Pick Alpine.js when you need reactive UI state without a build step — dropdowns, modals, accordions, client-side filtering. Pick HTMX when you want the server to drive page updates — CRUD flows, live search, inline editing, any pattern where returning HTML from the server is simpler than returning JSON and re-rendering it client-side. Use both on the same page when you need both.
Who this is for
Backend developers on server-rendered stacks (Laravel, Django, Rails, Astro) who want richer UIs without adopting React or Vue. If you are already running a full SPA, neither tool is the right question.
What we tested
Alpine.js v3.15.12 (released 2026-04-30) — 31.6k GitHub stars, ~548k weekly npm downloads, ~15 KB min+gzip, MIT. HTMX v2.0.9 (latest v2 stable, released 2026-04-20) — 48.1k GitHub stars, ~168k weekly npm downloads, ~14 KB min+gzip, BSD-2-Clause. Combined payload: ~29 KB min+gzip. Both tools tested against an Astro SSR project on Cloudflare Workers.
Mental models
These tools attack from opposite directions.
Alpine.js is Vue directives without a build step. State lives in the browser. You declare a reactive scope with x-data, bind DOM attributes with x-bind, respond to events with x-on, and toggle visibility with x-show/x-if. The browser runs it all; the server never sees it.
<!-- Alpine.js: client-state dropdown -->
<div x-data="{ open: false }">
<button x-on:click="open = !open">Menu</button>
<ul x-show="open">
<li><a href="/pricing">Pricing</a></li>
<li><a href="/docs">Docs</a></li>
</ul>
</div>
HTMX is hypermedia extended. You add attributes like hx-get, hx-post, and hx-swap to existing HTML elements. When the user interacts, HTMX fires a request to the server and swaps the response HTML into the page. There is no client state model — the server owns state, and the browser is a thin render layer.
<!-- HTMX: server-driven search -->
<input
type="search"
name="q"
hx-get="/search"
hx-trigger="input changed delay:300ms"
hx-target="#results"
placeholder="Search tools…"
/>
<div id="results"></div>
The mental model split is the most useful thing to internalize before reading specs or benchmarks.
Alpine.js vs HTMX: at a glance
| Alpine.js | HTMX | |
|---|---|---|
| Bundle (min+gzip) | ~15 KB | ~14 KB |
| State location | Browser | Server |
| Primary directive | x-data / x-bind | hx-get / hx-post / hx-swap |
| Build step required | No | No |
| JSON API needed | No | No |
| SSR compatible | Any stack | Any stack |
| Learning curve | Low — Vue devs: immediate | Low — HTML-fluent devs: immediate |
| GitHub stars | 31.6k | 48.1k |
| Weekly npm downloads | ~548k | ~168k |
| License | MIT | BSD-2-Clause |
| Ecosystem | Powers Tailwind UI, Laravel Livewire | Paris 2024 Olympics; Rails, Django, Laravel |
Code patterns
Modal toggle
Alpine.js: the right tool for a modal. No server round-trip needed.
<div x-data="{ showModal: false }">
<button x-on:click="showModal = true">Open modal</button>
<div
x-show="showModal"
x-transition
class="fixed inset-0 bg-black/50 flex items-center justify-center"
>
<div class="bg-white rounded-lg p-6 max-w-md w-full">
<h2 class="text-xl font-bold mb-4">Confirm action</h2>
<p class="mb-4">Are you sure you want to proceed?</p>
<div class="flex gap-2">
<button x-on:click="showModal = false" class="btn-primary">Confirm</button>
<button x-on:click="showModal = false" class="btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
Live search
HTMX: the right tool for a search that hits the database. The server returns a rendered <ul> fragment; HTMX swaps it in.
<!-- Client-side markup -->
<input
type="search"
name="q"
hx-get="/tools/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#search-results"
hx-indicator="#spinner"
placeholder="Search tools…"
/>
<span id="spinner" class="htmx-indicator">Searching…</span>
<ul id="search-results"></ul>
# Server-side: Django view returning an HTML fragment
def tool_search(request):
q = request.GET.get("q", "")
tools = Tool.objects.filter(name__icontains=q)[:10]
return render(request, "partials/tool_list.html", {"tools": tools})
Form POST with partial update
HTMX: posts the form, swaps the response into #feedback without a full page reload.
<form
hx-post="/tools/1/rate"
hx-target="#feedback"
hx-swap="outerHTML"
>
<input type="hidden" name="tool_id" value="1" />
<select name="rating">
<option value="5">★★★★★</option>
<option value="4">★★★★</option>
<option value="3">★★★</option>
</select>
<button type="submit">Submit rating</button>
</form>
<div id="feedback"></div>
The server returns the replacement <div id="feedback"> with the success or error message. One attribute does what used to require a fetch + DOM write.
Framework homes
Both tools work on any SSR stack, but adoption concentrates where they fit the workflow best.
HTMX is natural for: Rails (Hotwire alternatives), Django (class-based views returning partials), Laravel (Livewire and Blade partials), Astro (server endpoints). These stacks already return HTML from the server — HTMX is a thin wire between that output and the DOM swap.
Alpine.js is natural for: any SSR stack, but especially Laravel (first-class support in the ecosystem), Astro (drops into .astro files without a framework slot), Tailwind UI (all interactive components use Alpine). If you are already on Tailwind UI, Alpine is a forced choice — the components ship with Alpine markup.
Combining them
The recommended HTML-first stack is: server renders with your framework → HTMX moves data, Alpine manages UI state → Tailwind styles everything. Combined bundle: ~29 KB.
A working pattern:
<!-- HTMX loads the list; Alpine tracks the selected item client-side -->
<div x-data="{ selected: null }">
<button
hx-get="/tools"
hx-target="#tool-list"
hx-swap="innerHTML"
x-on:htmx:after-request="selected = null"
>
Refresh list
</button>
<ul id="tool-list">
<!-- HTMX swaps rendered items here -->
</ul>
<template x-if="selected">
<div class="detail-panel" x-text="selected.name"></div>
</template>
</div>
The caveat: when HTMX swaps a DOM node, Alpine state attached to the replaced nodes is destroyed. If HTMX replaces #tool-list and items inside had x-data scopes, those scopes are gone. Solutions:
- Attach Alpine state to a container HTMX never replaces (the parent
divabove). - Use
hx-swap="morphdom"(with the morphdom extension) — morphdom diffs the DOM instead of replacing it wholesale, preserving Alpine scopes on unchanged nodes. - For HTMX 2.x: use
hx-swap="outerHTML transition:true"with the View Transitions API for smoother swaps that also preserve more state.
The Alpine + HTMX combination is a solved problem, not a warning. One rule covers the gap.
Verdict
Pick Alpine.js if:
- You need client-side UI state: toggles, modals, accordions, tabs, form validation feedback.
- You are on a static site (Astro, Eleventy, Jekyll) and need interactivity without a JS framework.
- You use Tailwind UI — Alpine is part of the package.
- Your backend devs want to write HTML, not JSX.
Pick HTMX if:
- You want the server to own state and HTML generation.
- You are replacing JSON API + client-side fetch + DOM write patterns.
- Your stack is Laravel, Django, Rails, or any framework that renders HTML well.
- You are building CRUD, server-side search, pagination, or inline editing.
Use both if:
- You have server-driven data flows (HTMX) AND client UI state (Alpine) on the same page — common on anything beyond a pure content site.
- You are building on Cloudflare Workers or Pages and want minimal JS overhead — ~29 KB combined is as lean as you get while still covering both patterns.
Caveats
npm download numbers are not user counts. Alpine’s ~548k/week vs HTMX’s ~168k/week reflects automation, CI pulls, and Tailwind UI downloads more than direct adoption comparison. State of JS 2024 shows Alpine at 3% usage at work vs HTMX at 2% — closer than npm suggests.
HTMX v4.0.0-beta4 (published 2026-05-22) is the most recently published release on GitHub. Despite the “beta4” tag name, GitHub’s API marks it prerelease: false — the project does not flag it as a pre-release. v2.0.9 is the latest in the v2 stable line (released 2026-04-20). Before upgrading to v4 in production, check HTMX’s own release notes for the project’s stability assessment.
The article contains affiliate links to Cloudflare Pages/Workers. The verdict above would be identical without that relationship.
References
- Alpine.js docs
- Alpine.js GitHub — v3.15.12
- HTMX docs
- HTMX GitHub — v4.0.0-beta4 (latest, 2026-05-22)
- HTMX GitHub — v2.0.9 (latest v2 stable, 2026-04-20)
- npm trends: Alpine vs HTMX weekly downloads
- State of JS 2024 — front-end frameworks
- HTMX essays — Paris 2024 Olympics case study
- Alpine AJAX comparisons
- PkgPulse — HTMX vs Alpine.js 2026
- HTMX morphdom extension