My opinions on HTMX
Posted on 2025-06-24 in Programmation
HTMX is a JavaScript library designed to add interactivity to your web pages. With it, you don’t build a single page application, you use standard templates (written in Django, Jinja) rendered in your backend, add attributes to your HTML elements and let HTMX add the appropriate interactivity. From what I read, it’s getting lots of popularity in the Django world. That’s why I heard of it in the first place and one of the reason I choose it.
There are alternatives like Alpine.js or Turbo. After looking at their documentation and examples, I had the feeling HTMX is better suited for what I was building. HTMX also seems more popular and integration in Django is made easy thanks to django-htmx. It’s also quite small (at least compared to the cost of a SPA): only 17.5kb once minified and gzipped. It should be almost all the JS you need when you build something with it: if you need way more, switching to a SPA could make more sense. Likewise, if you need to build an app or handle complex state, a true SPA might be easier to maintain in the long run.
I think the library is powerful and relatively easy to use: all you have to do is include it and add the proper attributes. It also has lots of examples to help you get started. Pretty much everything I needed to use already had an example associated with it.
In my case, I use it in an article reading application: the backend fetches RSS feeds and displays the fetched articles on a page. The articles are grouped into various reading lists. To improve UX, when a user clicks on the "Mark as read" button, I don’t want a full page reload. That’s where HTMX comes in!
With HTMX, I can create a form and submit it to mark the article as read without this page reload. To do this, adding hx-boost and hx-swap to the form is enough to make HTMX handle the submission and handle the HTML from the response. A nice feature here is that you can update multiple part of the page in one go: the element associated with the update and something else thanks to a process named Out of Band Content swapping (oob for short): instead of just returning the element you need to swap, you can return multiple and HTMX will swap them all. In my case: the article card and the unread count are updated at the same time thanks to this process.
Two features required a bit more work to make them work correctly.
The first one is a feature, which when enabled, allows to read articles on scroll to easily scan big chunks of articles and only open those that interest you. This required some custom JS to correctly detect scroll end, wait a few seconds without interactions to trigger the action (to make sure the user won’t scroll up), find the articles that are not visible anymore and mark them all as read. It required some digging into the documentation to be able to hook on htmx and use its JavaScript API correctly. In the end, it worked really well!
The second one, is when users try to delete an article: I wanted to display a pretty modal and not a basic JS alert. Part of the problem comes from the fact that I have multiple possible action buttons and only for some I want the confirm modal to be triggered. This isn’t supported out of the box.
What I ended up doing was have multiple forms and use the form attribute of buttons to select which form the buttons must submit. This way, I can have a delete form with hx-confirm (use to display an alert or run a custom action on a click) and only open the modal in that case. As far as I can remember, putting hx-confirm on the button directly didn’t work (I don’t remember the exact behavior in that case). To have this work in all cases, I had to setup a custom event listeners on htmx:confirm to capture the event and run some custom processing code. I also had to make sure the name of the button was correctly transmitted in the request which wasn’t done automatically by HTMX in that case for some reason. The full code is available here if you need more details.
To write the proper solution, I had to debug HTMX. It’s a pain as in any library you don’t know but not a very big deal. In Django debug mode, I load the unminified version of the library and from there, I was able to add breakpoints from the browser tools and see what was happening. I had to do it another time when I tried an HTML minification library which broke HTMX completely. It turns out, it removed type="submit" attributes since it’s the default, but HTMX requires them to boost forms.
The library is great and I’m very pleased by it. There are however, as with any tech, some downsides:
HTMX behaviors can only be tested with end to end tests. Not much of an issue for me: they don’t change much and can be tested manually. It can be a problem on a bigger app, but I think it’s also hard to mess up something the smallest amount of manual testing would catch: you are only adding HTML attributes after all, the rest is handled by HTMX itself.
I think you must enable CSP (Content Security Policy) to make sure no HTML coming from an untrusted source is used by HTMX to swap elements and thus execute JavaScript. You should to it anyway, HTMX or not.
If you only enhance links and forms with hx-boost, your application will work without JS since browser have a fallback. If you do other things, JS becomes required since without it some behaviors will break. Not a very big deal in my opinion since all your users should have JS enabled. In my case, some menus managed by Bootstrap won’t open without JS, making the navigation in the app a pain anyway.
Since these attributes are specific to HTMX, I guess there is a form of vendor lock-in. But the attributes are easy to spot and since the alternatives also rely on attributes, I guess a search/replace would do most of the migration. It’s still way easier than changing SPA frameworks since it does way less!
The documentation promotes behaviors that are not accessible by default. To quote it:
Why should only <a> & <form> be able to make HTTP requests?
Why should only click & submit events trigger them?
Why should only GET & POST methods be available?
Why should you only be able to replace the entire screen?
I guess it’s up to you to make sure that what you do is accessible, just like with a SPA and use the proper attributes for accessibility. The examples could also promote accessibility more. Some already do, without explaining it enough in my taste, but most don’t. I think that accessibility is in such a bad place, that having all the official examples promote best practices would be a huge help.
According to this hacker news thread, the maintainers are aware of this and may improve the situation. There are also one issue about the accessibility implication of HTMX, one about improving the examples and there seems to have plans to use the new moveBefore API to improve the situation (this API is only available in Chrome and only since the beginning of the year).
To be fully honest, I don’t think my project is fully accessible. As a backend developer, I don’t know enough to see the issue (and I’m probably far from the only one if I trust my experience and the reports regarding the poor state of web accessibility). That’s also why, I rely as much as possible on forms with progressive enhancement and use Bootstrap which have an accessibility team. No matter the lib, most examples I see don’t even mention it. That’s not only an HTMX problem.
In conclusion, HTMX adds some complexity compared to basic pages, but it remains manageable. I’m happy with the lib and the fact it allows me to write most of my logic in Python and Django with standard HTML and almost no custom JavaScript. Now that the heavy lifting is done, it’s been months since I even had to touch HTMX features. Almost all my logic lives in the backend which feels easier and make me more productive: no language or context juggling. Plus, all the behaviors are defined in the HTML with the rest of the attributes. Depending on what you are building, it can be a good and more simple alternative to a full fledged SPA. It also gives me some memories of when I was a student and was building websites without a JS framework: use of plain JS files to extend your site’s features. It was a mess back then, even with the help of jQuery. Now, it feels clean and easy!