htmx is an excellent JavaScript library for building reactive front-ends and sending HTML over the wire. But it’s far more capable than that. It also comes with its own lesser-known JavaScript API that can replace the need for writing Vanilla JS or using an additional library such as Alpine.js.
htmx aims to provide a more “complete” hypertext, allowing us to add reactivity to our sites using HTML attributes. But one caveat is that it relies on AJAX requests, meaning a round-trip to a web server. This is great for a wide range of use cases and allows markup to be rendered on any back-end.
But sometimes, you may just want to manipulate the existing DOM. For this reason, when using htmx, it is not uncommon to fall back to Vanilla JS or a utility library such as hyperscript or Alpine.js.
If you’re already using htmx, then you probably don’t need another library, since it comes with its very own JavaScript API.
Let’s work through an example in which we want to toggle the visibility of a notification using a hidden
class whenever a button is clicked. We’ll explore several approaches before seeing how we can do it with htmx alone.
I’m a teapot.
Vanilla JS #
Here’s an example of toggling the visibility using pure (Vanilla) JavaScript.
<button onclick="document.getElementById('notification').classList.toggle('hidden')">
Toggle notification
</button>
<div id="notification" class="hidden">
I’m a teapot.
</div>
That’s not too bad, but there are some downsides. Besides the fact that inlining JavaScript is verbose and makes it difficult to maintain consistency, the execution of event handler attributes may be blocked by content security policies. Also, the events you can listen for are limited by HTML event attributes, so while listening for a click
event is trivial, listening for a htmx:afterOnLoad
event is not.
These downsides can, of course, be worked around by using a <script>
tag or by including a script file. But then we lose Locality of Behaviour, the principle that scripting logic should be co-located with the HTML elements the logic applies to.
hyperscript #
Here’s an example of toggling the visibility using hyperscript.
<button _="on click toggle .hidden on #notification">
Toggle notification
</button>
<div id="notification" class="hidden">
I’m a teapot.
</div>
What’s great about hyperscript is that it is terse, declarative, and reads like documentation. The downside is that it is an external dependency, still somewhat experimental, and that while reading hyperscript is easy, there’s a a learning curve involved in writing it.
hyperscript is designed to make it easy to respond to events and do simple DOM manipulation. It has a natural language style, is async transparent and can do so much more.
Alpine.js #
Here’s an example of toggling the visibility using Alpine.js.
<div x-data="{ visible: false }">
<button x-on:click="visible = !visible">
Toggle notification
</button>
<div id="notification" x-bind:class="!visible ? 'hidden' : ''">
I’m a teapot.
</div>
</div>
If you’ve worked with Vue.js before, this may feel familiar. If you haven’t, you may be wondering why there’s so much going on here.
The x-data
attribute defines the initial state of the variables (in this case, visible
, with a default value of false
) within the element it’s on. Clicking the button flips the value of visible
to its opposite (false
→ true
, true
→ false
), and the notification element’s class is “bound” to the value of visible
.
While Alpine.js is full of useful features, it takes a programmer’s mindset to work with, which may not come naturally to some people. I prefer to keep my HTML simple and concise, reserving logic to programming/scripting languages whenever possible.
Alpine.js is great for reactivity (it is powered by Vue.js’s reactivity engine), especially when you need to manage multiple reactive elements on the same page. If you think you need Vue.js or React, Alpine.js may save you hours of work and lots of lines of code.
jQuery #
For completion’s sake, here’s an example of toggling the visibility using jQuery.js.
<button onclick="$('#notification').toggleClass('hidden')">
Toggle notification
</button>
<div id="notification" class="hidden">
I’m a teapot.
</div>
I personally really like the jQuery syntax. However, it suffers from some of the same downsides as the inline JavaScript approach. The general sentiment is that modern JavaScript handles many of the things that jQuery set out to solve, and, let’s be honest, jQuery is so passé.
Excuse the snarky comment. I love jQuery and everything it did for modern JavaScript. It just feels too bloated for most projects. Interestingly, 12% of the people who responded to the State of JS Survey 2022 said they use jQuery regularly.
With htmx #
Finally, let’s see how you can achieve this with htmx’s JavaScript API.
<button hx-on:click="htmx.toggleClass('#notification', 'hidden')">
Toggle notification
</button>
<div id="notification" class="hidden">
I’m a teapot.
</div>
Subjectively speaking, this is just as readable and terse as the hyperscript and jQuery examples. But, assuming you already have htmx attributes in your HTML, you can simply continue adding more htmx attributes to achieve a fully reactive front-end.
The hx-on:*
attribute allows you to listen for any event, and the value can be any JavaScript code. Using htmx API methods here is optional, but it sure feels good!
For example, here’s how you might respond to a server error by listening for the htmx:responseError
event.
No teapot appears to exist, are they messing with us?!
<button hx-get="/teapot-no-exist" hx-on:htmx:response-error="htmx.removeClass('#error', 'hidden')">
Fetch a teapot
</button>
<div id="error" class="hidden">
No teapot appears to exist, are they messing with us?!
</div>
Since HTML attributes are case-insensitive, you should always use the kebab-case event name (
htmx:response-error
).A shorthand double-colon
hx-on::
exists for htmx events, so that the attribute can also be written ashx-on::response-error
.
Another example of a useful API method is takeClass
, which removes the given class from an element’s siblings and adds the class to itself. Here’s an example of using it with tabs, which can be annoying to implement in Vanilla JS.
<div>
<button hx-on:click="htmx.takeClass(event.target, 'active')" class="tab active">Content</button>
<button hx-on:click="htmx.takeClass(event.target, 'active')" class="tab">SEO</button>
<button hx-on:click="htmx.takeClass(event.target, 'active')" class="tab">Metrics</button>
</div>
We can save duplicating the hx-on:*
attribute by placing it on the parent element, since events bubble up in JavaScript.
<div hx-on:click="if (event.target.classList.contains('tab')) htmx.takeClass(event.target, 'active')">
<button class="tab active">Content</button>
<button class="tab">SEO</button>
<button class="tab">Metrics</button>
</div>
We added a condition above to ensure that the logic is only applied to elements containing a tab
class, for good measure.
Presentation #
I mentioned htmx and its JavaScript API in my recent presentation at Dot All 2023 and how you can use it for precisely these use cases, starting from minute 07:52. You can watch it below.
This part of the presentation is in the context of Sprig, a reactive component framework for Craft CMS that depends on htmx. Its syntax mirrors that of htmx, so you’ll see
s-on:click
, which is the same as usinghx-on:click
.
API Methods #
htmx’s API methods work well in HTML attributes but can also be used in stand-alone <script>
tags or JavaScript files.
Here are some of the more helpful API methods for working with the DOM (see the full API reference).
htmx.addClass()
#
Adds a class to the given element (or element selector).
htmx.addClass(element, 'hidden')
htmx.closest()
#
Finds the closest parent to the given element (or element selector) matching the selector.
htmx.closest(element, '.field')
htmx.find()
#
Finds a single element matching the selector.
htmx.find('#notification')
htmx.findAll()
#
Finds all elements matching a given selector.
htmx.findAll('.error')
htmx.on()
#
Creates an event listener on the given element (or element selector) or on the body
if the element is excluded.
htmx.on('htmx:responseError', event => console.log(event))
htmx.on(element, 'htmx:afterOnLoad', event => console.log(event))
htmx.remove()
#
Removes the given element (or element selector).
htmx.remove(element)
htmx.removeClass()
#
Removes a class from the given element (or element selector).
htmx.removeClass(element, 'hidden')
htmx.takeClass()
#
Takes the given class from its siblings, so that among its siblings, only the given element (or element selector) will have the class.
htmx.takeClass(element, 'active')
htmx.toggleClass()
#
Toggles a class from the given element (or element selector).
htmx.toggleClass(element, 'hidden')
htmx.trigger()
#
Triggers an event on an element (or element selector).
htmx.trigger(element, 'refresh');
htmx.values()
#
Returns the input values associated with the given element (or element selector).
htmx.values(element);
Conclusion #
As I stated in the presentation, htmx aims to pick up where HTML leaves off.
If following the grain of the web means embracing hypertext (HTML) and its transfer protocol (HTTP), then it naturally follows that we should embrace the grain of the tools we use to enhance it.
htmx gives you a “more complete” hypertext. Use it!
Fewer dependencies mean fewer things to learn, fewer things to maintain and fewer things that can go wrong. So before you reach for yet another JavaScript library, remember that good ole saying and keep it simple, stupid!