Typically, things like Alpine.js are compared to frameworks like Next.js because of their advantages over them.
Now weโre diving into something far more relevant. Both modules (Alpine.js and HMPL) work directly with HTML, and both are meant to bring logic closer to markup. That makes this comparison much more grounded.
Today, we're replacing Alpine.js in a project with HMPL.
Letโs get started! ๐๏ธ
โ๏ธ From simple examples
Letโs start with the most basic clicker example. With Alpine.js, youโd probably write something like this:
<div
x-data
x-fetch:clicker="{
url: '/click',
method: 'POST'
}"
x-init="$fetch.clicker"
class="clicker-container"
>
<h1>Alpine Clicker</h1>
<div class="click-count" x-text="$fetch.clicker.data?.count ?? 0"></div>
<button class="click-button" @click="$fetch.clicker">Click</button>
</div>
It looks great and, most importantly, works. Now letโs look at the HMPL.js version:
<body></body>
<script>
const templateFn = hmpl.compile(`
<div class="clicker-container">
<h1>HMPL Clicker</h1>
<div class="click-count" id="counter">
{{#request src="/click" after="click:#btn"}}
{{/request}}
</div>
<button id="btn">Click!</button>
</div>
`);
const clicker = templateFn().response;
document.querySelector("body").append(clicker);
</script>
As you can see, we have two different philosophies: Alpine.js works declaratively inside the HTML itself, while HMPL compiles templates and dynamically renders them via JS. But the interface is conceptually similar - actions tied to events, data updates in the DOM. The difference lies in control and flexibility.
Also, it would be great if you supported the project with your star! Thanks โค๏ธ!
๐ Built-in limitations
Alpine.js, while powerful in its simplicity, isn't inherently built around a server-oriented paradigm. It's a client enhancement tool meant for light interactivity, not full templating or server-side rendering. This makes it great for progressive enhancement but somewhat limiting when building applications that rely heavily on server-driven content.
Because Alpine doesn't natively treat the server as a central part of the UI lifecycle, integrating complex request flows or handling server state often feels bolted on. Even with plugins like @alpinejs/fetch
, you're still mostly wiring client-side logic, manually coordinating fetch calls, and managing reactive state in the browser.
This client-heavy approach can get cumbersome as your app scales. Form submissions, conditional loading, dynamic content updates, all require increasingly verbose JavaScript bindings or hacks involving x-init
, external methods, and nested directives. Alpine gives you control, but often without abstraction.
In contrast, a templating language like HMPL is designed with the server at the core. It embraces server-rendered HTML and fetch-driven reactivity natively. You don't have to manually coordinate state or requests - it's declarative, context-aware, and built for scenarios where the server and the UI work in unison.
๐ Complex example
Letโs take a more advanced example, a registration form with loading indicator. Here's what it might look like in Alpine.js:
<div x-data="formComponent()" id="wrapper">
<form @submit.prevent="submit">
<div class="form-example">
<label for="login">Login: </label>
<input type="text" name="login" id="login" x-model="form.login" required /><br/>
<label for="password">Password: </label>
<input type="password" name="password" id="password" x-model="form.password" required />
</div>
<div class="form-example">
<input type="submit" value="Register!" />
</div>
</form>
<template x-if="loading">
<div class="indicator">
<p>Loading...</p>
</div>
</template>
<div id="response" x-text="responseText"></div>
</div>
<script>
function formComponent() {
return {
form: { login: '', password: '' },
loading: false,
responseText: '',
async submit() {
this.loading = true;
const res = await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(this.form),
headers: { 'Content-Type': 'application/json' },
});
const text = await res.text();
this.responseText = text;
this.loading = false;
}
};
}
</script>
Now let's see how the same can be done in HMPL:
<body></body>
<script>
const templateFn = hmpl.compile(`
<div id="wrapper">
<form onsubmit="event.preventDefault();" id="form">
<div class="form-example">
<label for="login">Login: </label>
<input type="text" name="login" id="login" required /><br/>
<label for="password">Password: </label>
<input type="password" name="password" id="password" required />
</div>
<div class="form-example">
<input type="submit" value="Register!" />
</div>
</form>
<p>
{{#request
src="/api/register"
after="submit:#form"
repeat=true
}}
{{#indicator trigger="pending"}}
<div class="indicator">
<p>Loading...</p>
</div>
{{/indicator}}
{{/request}}
</p>
<div id="response">{{response.body}}</div>
</div>
`);
const initFn = (ctx) => {
const event = ctx.request.event;
return {
body: new FormData(event.target, event.submitter),
credentials: 'same-origin',
};
};
const result = templateFn(initFn);
document.body.append(result.response);
</script>
With HMPL, we gain granular control. You can intercept the event, access the FormData
, customize headers, control indicators, all within a declarative template structure.
๐ Size comparison
Let's not ignore the size difference.
Alpine.js is incredibly small, and with minimal functionality it stays tiny. Hereโs a quick visual:
๐ฆ Alpine.js:
๐ฆ HMPL:
So yes, Alpine lose the size game. But the gap isnโt massive โ and if you're building something more interactive or server-driven, the slight size increase might be worth the added flexibility.
Also, in a broader comparison, HMPL still performs surprisingly well:
Of course, the smaller the feature set, the smaller the final JS bundle, but that also means youโll need to write more imperative logic outside the HTML.
โ Conclusion
Alpine.js is a great choice for projects that need simple, inline reactivity without the complexity of larger frameworks. But in this article, I wanted to present an alternative.
HMPL offers a more modern, customizable approach to server-driven UI. If your app requires dynamic data handling, advanced request logic, or full fetch control โ HMPL might be a better fit.
In the end, itโs all about the right tool for the right job.
Thanks for reading this article โค๏ธ!
Whatโs your take on this comparison? Let me know in the comments!
Useful links:
Top comments (5)
Good article! First time I hear about Alpine.js, but it's probably a good module.
Thank you!
When replacing it, it would be possible to do less for Alpine.js if via
@alpinejs/fetch
, but then it would be more difficult to correctly configure RequestInit.Because of AlpineJS' html-first approach, it is the best to go with, the HPML still brings concepts of the other JS Frameworks around, which is a no no.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.