htmx is a UI library written in JavaScript that extends the features of HTML. It can be extended to make AJAX requests, do CSS transitions, connect to WebSockets and handle Server Sent Events inside an HTML document with just some declarative HTML attributes.
htmx was created by Carson Gross in 2013 because he found HTML to be limited for what he was trying to achieve. htmx tries to solve the following limitations of HTML:
- only
<a>
and<form>
tags are able to make HTTP requests - only
click
andsubmit
events trigger them - only
GET
andPOST
methods are available - and when using
<a>
and<form>
the whole screen gets replaced
In this blog, we will learn how htmx enhances HTML and overcomes the limitations mentioned above.
Note:
I will be using go for the code examples given in this blog post.
If you are not familiar with go you can learn the basics from here.
Throughout this blog I will be providing small snippets of my code instead of the actual implementation (you can find the code implementation here) and I will not go into how to create a webserver to serve the endpoints. For that you might want to read this blog.
Using htmx
We can simply use htmx by adding a script tag in the header of our HTML.
<head>
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
</head>
htmx claims to be small (~14k min.gz’d), dependency-free, extendable, IE11 compatible and 67% less code when compared with react.
Now let's see how to make a simple AJAX request in htmx.
Making AJAX request (GET)
So, let's begin by creating a few endpoints for this example.
func getRoot(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./static/index.HTML")
}
func hi(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<div>hi</div>")
}
func main() {
http.HandleFunc("GET /", getRoot)
http.HandleFunc("GET /hello", hi)
http.ListenAndServe(":3333", nil)
}
In the above code example, we made two endpoints: /
to serve the HTML file and /hello
will return an HTML element. htmx gets the body of the response and renders it on the page, therefore the response of our function should be HTML code.
Now we will see how to use the hello
endpoint in HTML:
<div hx-post="/hello">hello</div>
As seen above, the endpoint is working. The content of the div
is replaced by the response.
Because we did not define any specific event to trigger the AJAX request, it will be triggered by clicking the hello
text.
We can explicitly define it like this:
<div hx-get="/hello" hx-trigger="click">hello</div>
htmx has other predefined events like this:
-
hx-trigger="mouseenter"
is triggered every time that the mouse enters the element -
hx-trigger="mouseenter once"
is triggered when the mouse enters the element for the first time and won't trigger after that -
hx-trigger="click[ctrlKey]"
is triggered when the element is clicked while holding theCtrl
key -
hx-trigger="every 2s"
is triggered every two seconds
You can find more on this subject in the htmx docs.
Currently, our example above replaces the element that makes the AJAX request, but we can specify which element is going to get replaced using the hx-target
attribute:
<div id="replace" class="replace">replace me</div>
<div hx-get="/hello" hx-trigger="click" hx-target="#replace">click me to replace the element above</div>
We can also use a class name like .replace
or give elements like body
as the target.
In the following example we have two foo
elements:
<foo id="1" class="replace">replace me 1</foo>
<foo id="2" class="replace">replace me 2</foo>
<bar hx-post="/hello" hx-trigger="click" hx-target="foo">replace foo element</bar>
The first of the foo
elements will be taken as the target. So in this example the first foo
element with id=1
will be replaced by the response.
Making a todo app
Now we have learned the basics, let's write a todo app.
First start with making a submit form for adding a todo task.
For that, we will only need an input box and a button in our HTML code:
<div id="app" class="container">
<div id="todo-list"></div>
<form
id="myForm"
hx-post="/save"
hx-trigger="submit"
hx-target="#todo-list"
>
<input type="text" name="task" required />
<input type="submit" value="save" />
</form>
</div>
Now let's have a look at what is needed on the server side:
func save(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
insert(r.FormValue("task")) // save to database
taskList := "<div>" + r.FormValue("task") + " </div>"
io.WriteString(w, taskList)
}
func main() {
...
http.HandleFunc("/save", save)
...
}
In the HTML above, the form will send a post request to the /save
endpoint and htmx will replace the content in the div with the id todo-list
. The response will replace everything in the target element i.e., the element with id todo-list
will be replaced and looks like this:
<div id="todo-list">
<div>item 1</div>
</div>
After adding another todo item to the list, the HTML document looks like this:
<div id="todo-list">
<div>item 2</div>
</div>
That is not quite what we want. We want to append the new todo item after the last todo item. So we use the attribute hx-swap
to let htmx know where we want the response to render. The beforeend
value will append the HTML response before the end of the targeted element.
After the swap the code will look like this:
<div id="app" class="container">
<div id="todo-list"></div>
<form
id="myForm"
hx-post="/save"
hx-trigger="submit"
hx-target="#todo-list"
hx-swap="beforeend"
>
broadcast<input type="text" name="task" required />
<input type="submit" value="save" />
</form>
</div>
Adding the first item (item 1):
<div id="todo-list">
<div>item 1</div>
</div>
After adding the second item (item 2):
<div id="todo-list">
<div>item 1</div>
<div>item 2</div>
</div>
Adding multiple todo items is working now, but on page load (e.g. if we refresh the page) the list of tasks is not being loaded.
To make that work, we need to make a GET
request.
<div id="todo-list" hx-get="/taskList" hx-trigger="load"></div>
In the above HTML, I have added hx-get
with value '/tasklist' and hx-trigger
with value 'load'. The 'load' value specifies when the request is sent. In this case it's when the HTML file is loaded.
func listTask(w http.ResponseWriter, r *http.Request) {
tastList := getTODO() // gets data from the database and returns HTML
io.WriteString(w, tastList)
}
func main() {
...
http.HandleFunc("/save", listTask)
...
}
Now the list is shown even when the page is refreshed.
For edit let's try something different: instead of doing all our operations in a single page, we will be navigating to another page to do the edit.
First, let's make clicking on the todo item go to a different page that will contain a form for updating the todo item list.
For that, we need to get an anchor tag instead of a div with the todo text inside a div.
On the server side, I will get the task and return the proper form for updating that task.
func main() {
...
http.HandleFunc("/task/", individualTask)
...
}
Example response from server:
<div hx-boost="true">
<a href="/task/1">clean room</a>
</div>
Here the hx-boost
attribute will replace the body element and add the previous page to the history entry.
That means the back button on browsers will work perfectly.
For the actual editing, it will be similar to the other examples.
<div>
<div>clean room</div>
<form id="myForm2" hx-put="/edit" hx-target="#app">
<input type="text" name="task" required="" />
<input type="hidden" name="old" value="1" />
<input type="submit" value="edit" />
</form>
</div>
Now let's perform deletion on the list.
First, we have to make a delete button.
For that, I will update the list template and add a delete form:
<div hx-boost="true" class="new-element">
<a href="/task/1">clean room</a>
<form
hx-trigger="click"
hx-delete="/delete"
hx-target="closest .new-element"
hx-swap="outerHTML swap:1s"
>
<input type="hidden" name="task" value="1" />
delete
</form>
</div>
Most of the snippets in the delete form are similar to the other examples. But in the hx-swap="outerHTML swap:1s"
attribute, I added an extra value. This value will delay the swapping of the element by 1 second because I want to do a CSS animation during that time.
While htmx is doing the swapping of the element, it also adds the htmx-swapping
class to the element.
Now I can add any CSS effect I want to the htmx-swapping
class.
.htmx-swapping {
opacity: 0;
transition: opacity 1s ease-out;
}
WebSockets
Currently, if we add a todo task to our list, it won't be shown in another tab or browser opening the same page. This can be achieved using WebSockets. Fortunately htmx supports WebSockets.
<div id="ws" hx-ws="connect:/ws" hx-trigger="task"></div>
The hx-ws
attribute takes the WebSockets endpoint and makes a connection to it.
After adding this line to the code, I just have to create a WebSocket that listens for input by the user and broadcasts it to all clients.
import(
...
"golang.org/x/net/websocket"
"sync"
...
)
func main(){
...
server := NewServer()
http.Handle("/ws", WebSockets.Handler(server.WebSocketsHandler)) // handles the saving and broadcasting of data
...
}
Update the form:
<form
id="ws-form"
hx-ws="send:submit"
hx-target="#todo-list"
hx-swap="beforeend"
>
<input type="text" name="task" required />
<input type="submit" value="save ws" />
</form>
In the hx-ws
attribute of the form we are telling htmx to send the values to the WebSockets endpoint on the submit event.
func (s *Server) WebSocketsHandler(ws *WebSockets.Conn) {
broadcast() // broadcast the response
}
func main(){
...
http.Handle("/ws", WebSockets.Handler(server.WebSocketsHandler))
...
}
Now we can see our list getting updated everywhere.
These were some basics of htmx to get started with. Now you can build on it and make something more interesting by yourself.
And if you want to learn more about htmx, have a look at the docs here
Top comments (1)
This post has a lot of great examples of what HTMX can do, and I really liked how you explained its benefits. I think one of the benefits I would mention is that HTMX can work in tandem with HTML5 form validation, enabling developers to leverage native browser validation mechanisms. This integration simplifies the process of validating user input, ensuring that data submitted through forms meets certain criteria before being processed. HTMX enhances this by allowing asynchronous submission of forms and updating only parts of the form with validation messages, improving the feedback loop for the user.
If you are curious about how HTMX compares to other libraries such as Alpine.js and Knockout, that also simplify front-end interactions and make web apps more maintainable and performant, I recommend reading this article from Facundo Corradini that provides a very clear comparison and describes more about HTMX perks and limitations: scalablepath.com/front-end/htmx