DEV Community

Cover image for I replaced Alpine.js in my app with this alternative ๐Ÿ”ฅ
Anthony Max Subscriber for HMPL.js

Posted on

I replaced Alpine.js in my app with this alternative ๐Ÿ”ฅ

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 โค๏ธ!

๐Ÿ’Ž Star HMPL โ˜…


๐Ÿ” 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Alpine

๐Ÿ“ฆ HMPL:

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:

comparison

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)

Collapse
 
david_bussell14 profile image
David Bussell

Good article! First time I hear about Alpine.js, but it's probably a good module.

Collapse
 
anthonymax profile image
Anthony Max

Thank you!

Collapse
 
anthonymax profile image
Anthony Max

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.

Collapse
 
developerkwame profile image
Oteng Kwame

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.