Introduction
Single Page Applications (SPA) revolutionized the web development landscape and provided users emulation of native applications. With it however, also came added complexity. “Meta frameworks” such as NextJS or NuxtJS further add on to the complexity as developers are now tasked with managing complex interactions between server and client. In an attempt to reduce this complexity, HTMX has re-emerged into the scene.
What was formally called “intercooler.js” is now HTMX. HTMX in a nutshell is a small client side Javascript library that attaches attributes to DOM elements. These DOM elements are then used to send requests to the server. What this effectively allows the developer to do, is to emulate SPA reactivity by controlling it from server by sending small, HTML “snippets”, which then HTMX swaps or inserts somewhere in the DOM. This simple concept is enough to power many web applications, which do not require the large baggage that modern frontend frameworks come with.
Among many other benefits, this also reduces the need for managing state in two places (client and server), and also allows developers to code in any backend language they prefer.
In this post I’d like to highlight HTMX's simplicity by building a small todo list application together while discussing its benefits and why one should consider using it in their next projects.
A Todo List
The best way to understand HTMX is by building something with it. As is tradition, let’s build a todo list application with it. Here already we start with the inherent merit of HTMX and that is that we can use it together with any programming language of ones choice. This allows teams not to be locked into some framework that requires specialized infrastructure. In fact, many developers may choose HTMX simply to get away from JavaScript, and we’ll be doing just that with Python and Flask.
This combination offers a wonderful experience for web development, enabling the creation of web applications that emulate Single Page Applications (SPAs) with ease and efficiency. With Flask and HTMX, you can avoid the complexities associated with traditional build systems like webpack, while effortlessly managing state between the client and server. This approach is also ideal for developing proof of concepts at high velocity. We’ll do this with the coding platform Replit. If you wish to follow this on your local machine, please refer to this tutorial, but since the point of this article is to display HTMX’s ease of use, we will disregard application setup.
You can start right away with Replit by navigating to a new Repl and selecting the Flask template.
Let’s start right await by making an index template that wraps our entire page:
<!DOCTYPE html>
<!-- templates/index.html -->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
<script src="https://unpkg.com/htmx.org"></script>
<!-- Tailwind for styling -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="display flex flex-col items-center w-full relative h-screen">
<h1 class="text-3xl flex gap-4">
{% include 'star.html' %} Today
</h1>
<div id="todo-list">
<!-- This is where our todo list will go -->
</div>
<form hx-post="/add-todo" hx-target="#todo-list" hx-swap="“innerHTML”">
<button
type="submit"
class="text-white text-3xl absolute right-5 bottom-5 rounded-full bg-blue-600 aspect-square w-6"
>
+
</button>
</form>
</body>
</html>
And the following Python file that will contain all our logic:
# main.py
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
On the page we simply include HTMX via a script tag, no build step required.
Our first introduction to HTMX begins with the hx-post
, hx-target
, and hx-swap
attributes on the form
element. HTMX empowers web developers by allowing the addition of special attributes to any standard HTML element. These enhanced elements gain the ability to initiate HTTP requests. The responses in the form of HTML fragments, can then be dynamically inserted or replaced in any part of the webpage. This feature introduces a seamless way to update content without needing a full page refresh. In our case, we want to put our tasks into the div
element with the id of "todo-list".
Let’s do this by inserting an empty task when the user presses the “+” button. We start by adding an endpoint that will create the HTML fragment, and then return that fragment:
# main.py
@app.route('/add-todo', methods=["GET"])
def task():
return render_template('task.html')
And the following template:
<!-- templates/task.html -->
<div class="bg-gray-800 rounded-lg p-4 max-w-lg mx-auto my-6">
<div class="flex items-center">
<input
type="checkbox"
class="form-checkbox h-5 w-5 text-yellow-400 rounded border-gray-600"
/>
<input
type="text"
placeholder="New To-Do"
class="ml-4 bg-transparent border-0 placeholder-gray-400 text-white focus:outline-none"
style="width: calc(100% - 2rem);"
/>
</div>
<textarea
placeholder="Notes"
class="mt-2 bg-transparent border-0 placeholder-gray-400 text-white focus:outline-none w-full"
></textarea>
<div class="flex items-center justify-between mt-4">
<button class="text-yellow-400 focus:outline-none">
Today
</button>
<div class="flex space-x-2">
<button class="focus:outline-none">
{% include '/svg/line.html' %}
</button>
<button class="focus:outline-none">
{% include '/svg/dropdown.html' %}
</button>
<button class="focus:outline-none">
{% include '/svg/hamburger.html' %}
</button>
</div>
</div>
</div>
With some added styling, the actual important points of this fragment is the input
elements which will play a part soon. Clicking on the + button will now display a new, empty task. Let’s see how this works. The button looks like this:
<form hx-get="/add-todo" hx-target="#todo-list" hx-swap="“innerHTML”">
<button
type="submit"
class="text-white text-3xl absolute right-5 bottom-5 rounded-full bg-blue-600 aspect-square w-6"
>
+
</button>
</form>
The special attributes added by HTMX hx-get
, hx-target
and hx-swap
tells us, “when the form is submitted, issue a (hx-)get request, and (hx-)target the element with id “todo-list, and (hx-)swap the inner content of this HTML.”. The concept of emulating SPA-like behavior via attributes is what forms the entire behavior of HTMX. Here’s a few other attributes which would allow us to do other things:
hx-trigger
would allow us to trigger requests based on behavior, such as on mouse hover.hx-post
sends a post request instead of a get request. You can also do this with PUT, DELETE, and PATCH.hx-boost
allows us to convert all anchor tags and forms into AJAX requests that, by default, target the body of the page.hx-push-url
allows us to interact with the browser history API.
And many others. This also does not include the long list of values for each of these attributes which further allows us to customize behavior. It’s also important to note that these attributes can be applied to any element. These are all well documented in the documents page.
Let’s improve the todo list application by adding attributes to append tasks to the list, and to edit the individual tasks. Instead of swapping the entire inner HTML of the #todo-list
element, we can append to the end with beforeend
as such:
<form hx-get="/add-todo" hx-target="#todo-list" hx-swap="beforeend"
…
These tasks are ephemeral, so we need to start adding some state. Since HTMX allows us to control the front end from the server side, we don’t need to reach for state synchronization solutions as it is often needed in applications that use larger frontend frameworks. We’ll drive everything from the server as our source of truth:
# main.py
task_list = [{
"id": 222,
"title": "Schedule doctors appt",
"notes": "Make sure its in the evening after the 14th."
}]
@app.route('/todos', methods=["GET"])
def load_task():
return render_template('tasks.html', tasks=task_list)
We load our state into the template, and further split up our code into fragments, similar to components (create a file templates/tasks.html
):
<ul id="todo-list">
{% for task in tasks %} {% include 'task.html' with context %}
{% endfor %}
</ul>
The individual item templates are ready to consume the data supplied by Flask. This completes our "READ" process in our little CRUD application. Since we already have the "CREATE" process from our first code written, we can continue here onto our "UPDATE". We can add the ability to edit a task with a nice UX pattern. The user should be able to just type and the tasks will be updated seamlessly. We can do this by debouncing on key input. This can typically be quite terse with other frontend frameworks, requiring several lines and some intricate state updates, but with HTMX it is elegant:
<form
id="task-form-{{ task.id }}"
hx-post="/update-todo"
hx-trigger="keyup from:input, keyup from:textarea delay:500ms
"
>
<div class="flex items-center">
<input type="hidden" name="task_id" value="{{ task.id }}" class="" />
<input type="checkbox" class="“…”" name="checked" />
<input
type="text"
name="title"
placeholder="New To-Do"
class="“…”"
value="{{ task.title if task.title }}"
/>
</div>
<textarea name="notes" placeholder="Notes" class="“…”">
{{ task.notes if task.notes }}</textarea
>
</form>
The hx-trigger
on top of the form does a lot of work for us here, and it almost reads like in plain English, “trigger on key from input or textarea, and delay for 500 milliseconds”.
Finally, we can update the data on our backend:
@app.route('/update-todo', methods=["POST"])
def update_todo():
# Extract the data from the form
task_id = request.form.get('task_id')
title = request.form.get('title')
notes = request.form.get('notes')
checked = request.form.get('checked') == 'on'
# Update the task in the list with some fun list comprehension
global task_list
task_list = [{
**task, 'title': title,
'notes': notes,
'checked': checked
} if str(task['id']) == task_id else task for task in task_list]
# return nothing so to disallow default swapping from HTMX
return '', 204
And with that, we've concluded our "UPDATE" process code. I leave the exercise of deletion and sorting (or whatever else you may think of) to the reader. If you’ve been convinced by now, you can probably already see how trivial it can be to add the rest of these features.
Up until now we've explored how to do some simple CRUD operations. One reason however that people chose to use the complex modern frontend frameworks is the ability to have state dependent on other state update automatically. While HTMX may not be the best approach here when requiring a complex set of dependencies across many DOM elements, it is still absolutely achievable to accomplish this with HTMX. HTMX provides the hx-swap-oob
attribute that lets you swap outside of the target element. As the HTMX documentation says:
This allows you to piggy back updates to other element updates on a response.
With this in mind, we can add a great example for our little app. What we will add is a counter at the top of the application header. This counter is a state that is dependent on the number of todo items. When we add a new todo item, we can update the top header with the new updated count value:
At the end of the task.html
file we add:
<!-- rest of HTML -->
{% if new_task_length %}
<span id="counter" hx-swap-oob="true">{{ new_task_length }}</span>
{% endif %}
And then we add the following item to the return statement of the /add-todo
endpoint:
@app.route('/add-todo', methods=["GET"])
def task():
# ...rest of logic
return render_template('task.html', task={}, new_task_length=len(task_list))
this demonstrates how easy it is to manage state dependent updates via HTMX's hx-swap-oob
feature. Here, with just a few lines of code, we have created an additional feature in our app - a counter that automatically updates based on the current length of our task list. The length of the task list is a dependent state, reflecting the total count of todo items. When a new task is created, the counter in the header is automatically refreshed to display the new count.
I want to conclude this tutorial exercise section by discussing animations, one reason why many people choose to use Javascript frameworks.
![Github Branches](/assets/img/articles/2023-12-19-Introduction-to-HTMX/img-2.webp
Indeed, animations are a key aspect of modern web applications, often being a primary reason for choosing JavaScript frameworks. However, with HTMX, you don't have to forgo this essential element of user experience.
Adding Animations with HTMX
Animations can be achieved through various methods while using HTMX. A significant portion of interactive feedback can be handled with CSS. Modern CSS provides enough for creating animations and transitions, making it possible to animate changes in your UI in response to HTMX events.
For instance, using CSS transitions, you can smoothly animate the appearance and disappearance of elements, or their transformation in response to user interactions. This can be particularly effective for operations like adding, updating, or deleting items in a list, where visual feedback enhances the user experience. With HTMX, this is taken a step further with great integrations such as swap transitions. Take an example from the docs:
If you want to fade out an element that is going to be removed when the request ends, you want to take advantage of the
htmx-swapping
class with some CSS and extend the swap phase to be long enough for your animation to complete. This can be done like so:
<style>
.fade-me-out.htmx-swapping {
opacity: 0;
transition: opacity 1s ease-out;
}
</style>
<button
class="fade-me-out"
hx-delete="/fade_out_demo"
hx-swap="outerHTML swap:1s"
>
Fade Me Out
</button>
Leveraging JavaScript Libraries
For more complex animations, you can integrate lightweight JavaScript libraries. GreenSock (GSAP) is an excellent example of a library that provides advanced animation capabilities with a simple API. By combining HTMX with such libraries, you can create sophisticated animations that bring your UI to life, without the overhead of a full JavaScript framework.
Lastly, I'd like to point out the emerging Transitions API, currently only available in Chrome. This API is set to offer native support for smooth transitions, potentially reducing the reliance on external libraries for such effects, thus making the necessity of SPA frameworks less relevant. HTMX too, integrates well as it provides access to this API via the hx-swap
attribute, which you can learn more about here.
Wrapping Up
HTMX offers a sublime experience for developers with simplicity and functionality. It allows for developers to focus on business logic without compromising on UI/UX. With HTMX, basic REST knowledge is enough to build a fully functional user interface, reducing complexity, development time, and costs.
HTMX isn't a solution for everything, though. For more complex applications, a comprehensive JavaScript framework may be more suitable. The state management and component-based architectures that HTMX doesn't inherently support might be a better fit depending on the application and needs.
Despite this, HTMX's ease of integration and minimalistic approach makes it a valuable tool that should always be considered. It's particularly effective for enhancing server-rendered pages or for projects where a full JavaScript framework is unnecessary. Ultimately, HTMX stands out for its ability to deliver efficient, interactive web experiences with minimal overhead, making it a technology worth considering in various web development scenarios.
Since there is always more to learn, please consider checking out the official documentation and the essays as well as the free book provided by the HTMX author. You can also visit the source code used for this project here: Replit
Resources
Article Photo by HTMX
Author
Hans Hofner
Web Developer