HTMX + Alpine.js Patterns Cheat Sheet
Common patterns for pairing HTMX's server-driven HTML swaps with Alpine.js's lightweight client-side reactivity.
HTMX Basic Request + Swap
Fetch server-rendered HTML and swap it into the DOM without a page reload.
<div id="results"> <input type="text" name="q" hx-get="/search" hx-trigger="keyup changed delay:300ms" hx-target="#results" hx-swap="innerHTML" hx-indicator="#spinner"> <span id="spinner" class="htmx-indicator">Loading...</span></div>
Alpine.js Local UI State
Handle purely client-side toggles (menus, tabs) with x-data, no server round trip needed.
<div x-data="{ open: false }"> <button @click="open = !open" :aria-expanded="open">Menu</button> <ul x-show="open" x-transition x-cloak> <li>Profile</li> <li>Settings</li> </ul></div>
HTMX Triggers Alpine State
Use HTMX's hx-on to update an Alpine store after a server response lands.
<div x-data="{ count: 0 }"> <button hx-post="/like" hx-swap="none" hx-on::after-request="if(event.detail.successful) count++"> Like (<span x-text="count"></span>) </button></div><script>document.addEventListener('alpine:init', () => { Alpine.store('cart', { items: [] })})</script>
Out-of-Band Swap for Toasts
Update a notification region from any HTMX response, independent of the main target.
<!-- server response fragment --><div id="main-content">Item saved.</div><div id="toast" hx-swap-oob="true" x-data x-init="setTimeout(() => $el.remove(), 3000)"> Saved successfully!</div>
Key Attributes Cheat Table
The attributes you'll use in nearly every HTMX + Alpine page.
- hx-get / hx-post / hx-put / hx-delete- issues the corresponding HTTP verb and swaps the response
- hx-trigger- controls the DOM event that fires the request (e.g. `click`, `keyup changed delay:500ms`)
- hx-target / hx-swap- where and how (innerHTML, outerHTML, beforeend...) the response is placed
- x-data- declares an Alpine component's reactive scope
- x-show / x-if- conditional visibility (x-show toggles CSS, x-if removes from DOM)
- x-model- two-way binds form input to Alpine state
- hx-boost- progressively enhances normal <a>/<form> into AJAX navigation
Reusable Components with Alpine.data()
Register a named, reusable component factory instead of repeating inline x-data objects across markup.
document.addEventListener('alpine:init', () => { Alpine.data('dropdown', () => ({ open: false, toggle() { this.open = !this.open }, close(e) { if (!this.$el.contains(e.target)) this.open = false }, }))})
hx-sync to Prevent Request Races
Coordinate overlapping triggers on related elements so only one in-flight request wins, avoiding out-of-order swaps.
<form hx-post="/validate" hx-trigger="submit" hx-target="#result"> <input name="email" hx-post="/validate/email" hx-trigger="keyup changed delay:400ms" hx-sync="closest form:abort" hx-target="#email-error"> <button type="submit">Submit</button></form>
Server-Sent Events Extension
Stream server-pushed HTML fragments into the page over a persistent connection, without polling.
<div hx-ext="sse" sse-connect="/events/notifications"> <div sse-swap="message" hx-swap="beforeend"></div></div><!-- server sends: --><!-- event: message\ndata: <div class="toast">New order #42</div>\n\n -->
$watch and $nextTick for Derived Side Effects
React to state changes and safely read post-render DOM measurements.
<div x-data="{ query: '', results: [] }"> <input x-model="query"> <script> // inside an Alpine component's init(), or via x-init: // this.$watch('query', (value, oldValue) => { fetchResults(value) }) </script> <div x-init="$watch('results', () => $nextTick(() => $el.scrollTo(0, 0)))"> </div></div>
Advanced Attributes & Lifecycle Events
The escape hatches you need once basic swaps and toggles aren't enough.
- hx-vals / hx-headers- attach extra JSON-encoded parameters or headers to a request, evaluated as JS with `js:` prefix
- hx-confirm- shows a native confirm() dialog before issuing the request; pair with hx-on::confirm to customize
- hx-push-url / hx-replace-url- updates the browser history/URL bar after a swap for bookmarkable AJAX views
- htmx:responseError / htmx:sendError- fired on non-2xx responses or network failure; the default place to show a global error toast
- htmx:configRequest- fires before a request is sent, lets you mutate headers/parameters (e.g. inject a CSRF token globally)
- x-effect- reruns a snippet whenever any reactive dependency it reads changes, like a lightweight $watch
- Alpine.store()- global reactive store shared across components, useful for state HTMX doesn't own (e.g. a cart badge)
Keep server state (data that must be consistent across users/sessions) in HTMX swaps and keep Alpine strictly for ephemeral, per-client UI state — mixing the two sources of truth for the same value is the #1 cause of bugs in this stack.