HTMX + Alpine.js: Building a SPA Feel Without a Client-Rendered App
Our marketplace uses no React, Vue, or Svelte application. Most of the interactive UI is HTMX and Alpine.js layered on server-rendered HTML. Alpine is a JavaScript framework, but it does not turn the site into a client-r
Our marketplace uses no React, Vue, or Svelte application. Most of the interactive UI is HTMX and Alpine.js layered on server-rendered HTML. Alpine is a JavaScript framework, but it does not turn the site into a client-rendered SPA.
The Principle
Every navigable page URL can return a complete page rendered with Go and Templ. HTMX progressively enhances navigation by replacing fragments. Alpine.js handles local UI state such as dropdowns and modals.
There is no virtual DOM or hydration step. We still copy or minify our JavaScript for deployment, and an isolated rich-text editor has its own bundle; HTMX and Alpine do not require a framework-specific compile pipeline.
HTMX Navigation: Catalog Example
The catalog keeps an ordinary GET form. Global hx-boost progressively enhances links and forms, while the inherited target replaces the page's main content:
<body hx-boost="true"
hx-target="#main-content"
hx-swap="innerHTML transition:true show:window:top">
<main id="main-content">
<form method="get" action="/en/vehicles/cars/for-sale">
<select name="brand">
<option value="">All brands</option>
<option value="toyota">Toyota</option>
</select>
<button type="submit">Apply</button>
</form>
<!-- Server-rendered catalog cards -->
</main>
</body>
Without JavaScript, the form performs normal navigation. With HTMX, hx-boost sends an HX-Request header and swaps #main-content. The same URL remains directly navigable and crawlable.
The form does not hardcode hx-push-url. When query facets can be represented by a canonical path, the server returns the HTMX response with HX-Push-Url; a normal request receives the corresponding redirect. This keeps URL normalization at the HTTP boundary instead of duplicating it in browser code.
Alpine.js: Client-Side State
Alpine handles interactions that do not need a server round trip:
<!-- Mobile menu toggle -->
<div x-data="{ open: false }">
<button @click="open = !open"
:aria-expanded="open.toString()"
aria-controls="mobile-menu">
Menu
</button>
<nav id="mobile-menu" x-show="open" x-transition>
<!-- menu items -->
</nav>
</div>
Our Alpine components are registered from a plain JavaScript file. That is a project choice, not an Alpine requirement.
Notifications via HTMX Triggers
When a server action completes, it can send a small UI event through an HTMX response header. The public and account UI uses show-flash (the admin UI has an analogous show-toast event):
func SendSiteFlash(c *gin.Context, message string, isError bool) {
payload := map[string]any{
"show-flash": map[string]any{
"message": message,
"error": isError,
},
}
data, _ := json.Marshal(payload)
c.Header("HX-Trigger", ToHeaderASCII(string(data)))
}
On the client, Alpine listens for the event and updates an accessible status element:
<div x-data="{ msg: '', err: false, show: false }"
@show-flash.window="msg = $event.detail.message; err = !!$event.detail.error; show = true"
x-show="show"
:role="err ? 'alert' : 'status'">
<span x-text="msg"></span>
</div>
JSON encoding protects the header's structure, while ASCII conversion keeps the HTTP header transport-safe. Neither step makes sensitive provider errors suitable for users, so map those to controlled messages first and keep header payloads small.
Why This Stack?
- Progressive enhancement. Important content and links exist in server-rendered HTML.
- A smaller client runtime. We ship only the libraries and custom behavior the page needs. The actual payload should be measured, not assumed.
- A direct mental model. The server returns HTML, HTMX swaps it, and Alpine owns small pockets of local state.
Complex client-side interactions can still justify a focused library. We use a rich-text editor as an isolated module rather than making the entire site a client application.
One boundary matters: Alpine validation is only a usability enhancement. Authorization and input validation still belong on the server.
This article is based on lessons from building Towami, a multi-country marketplace and white-label storefront platform. Follow for more practical notes on Go, HTMX, infrastructure, and SaaS.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.