Htmx Alpine Integration
Combining Alpine.js with HTMX for client-side state and interactivity alongside hypermedia-driven updates
You are an expert in combining Alpine.js with HTMX to build applications that use server-driven HTML updates for data and Alpine.js for lightweight client-side interactivity. ## Key Points - **HTMX** owns the network: fetching HTML from the server and swapping it into the DOM. - **Alpine.js** owns local UI state: showing/hiding elements, toggling classes, managing ephemeral client-side state that is not persisted. - **Use `morph` swaps to preserve Alpine state.** When HTMX updates content that contains Alpine components, `morph` patching updates the DOM in place without destroying Alpine's reactive state. - **Bridge HTMX and Alpine via custom events.** Use `HX-Trigger` response headers to fire events that Alpine listens for with `@event-name.window`. This keeps the two libraries decoupled. - **Load Alpine after HTMX.** Alpine's `defer` attribute ensures it initializes after the DOM is ready. Its MutationObserver then catches any elements HTMX inserts later. - **Use `x-transition` for smooth swaps.** Alpine's transition directives work on elements that appear/disappear via `x-show` or `x-if`, complementing HTMX's `hx-swap` timing modifiers. ## Quick Example ```html <!-- Load Alpine AFTER HTMX so Alpine's MutationObserver catches HTMX swaps --> <script src="https://unpkg.com/htmx.org@2.0.0"></script> <script defer src="https://unpkg.com/alpinejs@3.14.0"></script> ``` ```python # Server response after successful save response = HttpResponse(status=204) response["HX-Trigger"] = "closeModal, contactCreated" ```
skilldb get htmx-skills/Htmx Alpine IntegrationFull skill: 264 linesAlpine.js + HTMX Integration — HTMX
You are an expert in combining Alpine.js with HTMX to build applications that use server-driven HTML updates for data and Alpine.js for lightweight client-side interactivity.
Overview
HTMX handles server communication and DOM updates, while Alpine.js provides client-side reactivity for UI state that does not need to touch the server (toggling dropdowns, managing tabs, animating transitions). Together, they form a lightweight full-stack approach that avoids heavy JavaScript frameworks while delivering rich interactivity. The key integration challenge is ensuring Alpine.js components survive or reinitialize after HTMX swaps new content into the DOM.
Core Concepts
Separation of Concerns
- HTMX owns the network: fetching HTML from the server and swapping it into the DOM.
- Alpine.js owns local UI state: showing/hiding elements, toggling classes, managing ephemeral client-side state that is not persisted.
The Reinitalization Problem
When HTMX swaps new HTML into the DOM, Alpine components inside that HTML need to be initialized. Modern Alpine.js (v3+) uses a MutationObserver and automatically initializes x-data components when they appear in the DOM, so in most cases no extra configuration is needed.
Morph Swaps
HTMX's morph swap strategy (via the idiomorph extension) intelligently patches the DOM rather than replacing it wholesale. This preserves Alpine.js state across swaps because the existing elements are updated in place rather than destroyed and recreated.
Implementation Patterns
Loading Script Order
<!-- Load Alpine AFTER HTMX so Alpine's MutationObserver catches HTMX swaps -->
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<script defer src="https://unpkg.com/alpinejs@3.14.0"></script>
Dropdown Menu with Server-Loaded Content
<div x-data="{ open: false }" class="dropdown">
<button @click="open = !open" class="dropdown-toggle">
Notifications
<span hx-get="/notifications/count"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
class="badge">0</span>
</button>
<div x-show="open"
x-transition
@click.outside="open = false"
class="dropdown-menu">
<div hx-get="/notifications/list"
hx-trigger="intersect once"
hx-swap="innerHTML">
Loading notifications...
</div>
</div>
</div>
Alpine manages the open/close state; HTMX loads the notification count and list from the server.
Tabs with Server-Rendered Content
<div x-data="{ activeTab: 'overview' }">
<nav class="tabs">
<button @click="activeTab = 'overview'"
:class="{ 'active': activeTab === 'overview' }"
hx-get="/project/1/overview"
hx-target="#tab-content"
hx-trigger="click"
hx-swap="innerHTML">
Overview
</button>
<button @click="activeTab = 'members'"
:class="{ 'active': activeTab === 'members' }"
hx-get="/project/1/members"
hx-target="#tab-content"
hx-trigger="click"
hx-swap="innerHTML">
Members
</button>
<button @click="activeTab = 'settings'"
:class="{ 'active': activeTab === 'settings' }"
hx-get="/project/1/settings"
hx-target="#tab-content"
hx-trigger="click"
hx-swap="innerHTML">
Settings
</button>
</nav>
<div id="tab-content">
<!-- Server-rendered content appears here -->
</div>
</div>
Modal Dialog
<div x-data="{ showModal: false }">
<button @click="showModal = true"
hx-get="/contacts/new"
hx-target="#modal-body"
hx-swap="innerHTML">
New Contact
</button>
<!-- Modal backdrop -->
<div x-show="showModal"
x-transition.opacity
@keydown.escape.window="showModal = false"
class="modal-overlay">
<div class="modal-content"
@click.outside="showModal = false"
x-transition.scale>
<div id="modal-body">
<!-- HTMX loads form here -->
</div>
</div>
</div>
</div>
Close the modal after successful form submission using HX-Trigger:
# Server response after successful save
response = HttpResponse(status=204)
response["HX-Trigger"] = "closeModal, contactCreated"
<div x-data="{ showModal: false }"
@close-modal.window="showModal = false">
<!-- ... modal markup ... -->
</div>
Preserving Alpine State with Morph Swaps
<head>
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<script src="https://unpkg.com/idiomorph@0.3.0/dist/idiomorph-ext.min.js"></script>
<script defer src="https://unpkg.com/alpinejs@3.14.0"></script>
</head>
<body hx-ext="morph">
<div x-data="{ editing: false, count: 0 }">
<div hx-get="/items"
hx-trigger="load"
hx-swap="morph:innerHTML"
hx-target="this">
<!-- Content morphed in place; Alpine state (editing, count) survives -->
</div>
<p>Local counter: <span x-text="count"></span></p>
<button @click="count++">Increment</button>
</div>
</body>
Alpine Store with HTMX Events
<script>
document.addEventListener("alpine:init", () => {
Alpine.store("cart", {
count: 0,
update(newCount) {
this.count = newCount;
}
});
});
// Listen for HTMX-triggered events to update Alpine stores
document.body.addEventListener("cartUpdated", (evt) => {
Alpine.store("cart").update(evt.detail.count);
});
</script>
<!-- Cart badge uses Alpine store -->
<span x-data x-text="$store.cart.count" class="cart-badge"></span>
<!-- Add-to-cart button uses HTMX -->
<button hx-post="/cart/add/42"
hx-swap="none">
Add to Cart
</button>
Server response:
response = HttpResponse(status=204)
response["HX-Trigger"] = json.dumps({"cartUpdated": {"count": 5}})
Inline Editing with Alpine Transitions
<div x-data="{ editing: false }" class="editable-field">
<template x-if="!editing">
<div>
<span id="contact-name">John Doe</span>
<button @click="editing = true">Edit</button>
</div>
</template>
<template x-if="editing">
<form hx-put="/contacts/1/name"
hx-target="#contact-name"
hx-swap="innerHTML"
@htmx:after-swap.window="editing = false">
<input name="name" value="John Doe" x-transition>
<button type="submit">Save</button>
<button type="button" @click="editing = false">Cancel</button>
</form>
</template>
</div>
Core Philosophy
The HTMX-Alpine combination is built on a clear division of labor: HTMX handles all communication with the server, and Alpine handles all ephemeral UI state on the client. This separation prevents the two libraries from competing for the same responsibilities and creates a mental model where data flows from the server through HTMX while visual interactions are managed locally by Alpine.
This pairing works because both libraries share a philosophy of progressive enhancement through HTML attributes. Neither requires a build step, a component model, or a virtual DOM. You add hx-get for server interactions and x-data for client interactions, and both degrade gracefully without JavaScript. The result is a full-featured interactive application built entirely through declarative HTML attributes.
The key architectural challenge is managing the boundary where HTMX swaps meet Alpine state. When HTMX replaces a chunk of DOM, any Alpine components inside that chunk are destroyed unless you use morph swaps or keep the x-data container outside the swap target. Understanding this boundary is the difference between a smooth integration and one plagued by disappearing state and broken reactivity.
Anti-Patterns
-
Using Alpine for data fetching. Making
fetch()calls inside Alpine components when HTMX could do the same with an attribute creates two competing data-fetching layers. Let HTMX handle all server communication and use Alpine only for local UI state. -
Building complex Alpine stores for data that belongs on the server. If your Alpine store grows beyond a few reactive properties, the state probably belongs in a server-rendered response rather than in client-side memory. Complex client state is a sign that you should let the server own more of the truth.
-
Placing
x-datacontainers inside HTMX swap targets. When HTMX replaces the inner HTML of a target, any Alpine state inside that target is destroyed. Placex-dataon an ancestor outside the swap target, or use morph swaps to preserve state across updates. -
Including Alpine's script tag in HTMX response fragments. If HTMX swaps in HTML that re-imports Alpine, components may double-initialize. Alpine should be loaded once in the page head and never included in server-rendered fragments.
-
Ignoring event name case conventions between HTMX and Alpine. HTMX converts
HX-Triggerheader names to lowercase kebab-case DOM events. A trigger namedcloseModalfires asclosemodal, and Alpine listens with@close-modal.window. Misunderstanding this mapping causes silent event handling failures.
Best Practices
- Let HTMX own data, Alpine own UI state. If a piece of state must persist or is shared across users, it belongs on the server (HTMX). If it is purely visual and ephemeral (open/closed, hover, animation), it belongs in Alpine.
- Use
morphswaps to preserve Alpine state. When HTMX updates content that contains Alpine components,morphpatching updates the DOM in place without destroying Alpine's reactive state. - Bridge HTMX and Alpine via custom events. Use
HX-Triggerresponse headers to fire events that Alpine listens for with@event-name.window. This keeps the two libraries decoupled. - Load Alpine after HTMX. Alpine's
deferattribute ensures it initializes after the DOM is ready. Its MutationObserver then catches any elements HTMX inserts later. - Keep Alpine components small. If a component grows beyond a few reactive properties, it may be a sign that the state should live on the server. Prefer server rendering over complex client-side state management.
- Use
x-transitionfor smooth swaps. Alpine's transition directives work on elements that appear/disappear viax-showorx-if, complementing HTMX'shx-swaptiming modifiers.
Common Pitfalls
- Alpine state lost after
innerHTMLorouterHTMLswap. Standard HTMX swaps destroy and recreate DOM nodes, which resets Alpine state. Usemorphswaps or restructure so thex-datacontainer is outside the swap target. - Double initialization. If Alpine is loaded before HTMX and then HTMX swaps in HTML containing
<script>tags that re-import Alpine, components may initialize twice. Never include Alpine's script tag in HTMX fragments. - Event name case sensitivity. HTMX converts
HX-Triggerevent names to lowercase and kebab-case for DOM events. AnHX-Trigger: closeModalheader fires aclosemodalDOM event. In Alpine, listen for@close-modal.window(Alpine handles kebab-case to camelCase mapping). - Using Alpine for data fetching. Avoid
fetch()calls inside Alpine components when HTMX can do the same with an attribute. Mixing two data-fetching approaches creates confusion about which layer is responsible. - Overcomplicating with Alpine stores. Alpine stores are useful for cross-component state, but most state should live on the server. If you find yourself building a complex client-side store, reconsider whether the server should own that state.
Install this skill directly: skilldb add htmx-skills
Related Skills
Htmx Active Search
Active search, typeahead, and autocomplete patterns using HTMX triggers and debouncing
Htmx Backend Patterns
Server-side patterns for HTMX including partial templates, response headers, and middleware
Htmx Basics
Core HTMX attributes and fundamental patterns for hypermedia-driven web development
Htmx Forms
Form handling, validation, and submission patterns with HTMX
Htmx Infinite Scroll
Infinite scroll and lazy loading patterns using HTMX revealed and intersect triggers
Htmx Progressive Enhancement
Progressive enhancement strategies for building HTMX applications that work without JavaScript