Building Progressively Enhanced Forms Using htmx
; Free of LLM-generated textI recently built a new form for ties, with lots of interactivity. It’s for editing a bookmark and connecting it to lists:
The feature uses progressive enhancement: it works without JS, but uses HTMX to add some niceties when JS is enabled.
In this post, I’ll go through my approach and talk about a few things to keep in mind when building a UI in this style.
Transient State
Interfaces that work without JS are fundamentally different from single page applications: updating the UI in response to user interactions will often require a roundtrip to the server
(usually initiated by a or form elements).
Before the user completed their task and you’ve saved something to the database, you’ll have “transient state”: Input that the user entered, that isn’t persisted (yet), and that needs to survive this roundtrip.
One example from the form described above would be a new name for the bookmark. When the user hits the “rename” button, the name is sent to the server, and if there are validation errors, the server will return a response showing an error. The response includes the users input value, so they don’t have to type it again.
In an SPA, transient state would live in JS memory, and you’d use some library with an unholy amount of dependencies to manage it. Since we’re making do without JS, we’ll have to use classic HTML features to manage this state, each with its own set of tradeoffs.
Thinking about how this transient state flows through the system will help mapping complex interactions to HTML.
Form Values
This is used for the “rename” field from the example, and the most commonly used one. When the form is submitted, the server receives these values and can include them in its response so they don’t vanish when the user interacts with the server.
It’s simple, and most likely the technique you’re already using for lots of user input.
A drawback of this technique is that values will disappear if the user reloads the page. Another one is that values are only submitted for one form - if you have multiple forms on a single page, values from the other forms will vanish on submit. For example, the bookmark edit form above is split into two forms: one for renaming the bookmark, and another one assigning lists (I’ll go into the reasons for the split later on). You can work around this using HTMX, so users without JS will experience graceful degradation.
Query Parameters and Paths
By putting state into the path or query parameters of the page, it will survive across page reloads. It will also survive bookmarking, being shared as a link, etc.
This is a good usecase for e.g. search queries, like the “search for lists to connect” input in the form above.
The most common ways to set the path and query parameters are links’ href attributes and forms’ action attributes.
You can also use a form with the method="GET" attribute which will put all input values of that form into the query string.
Lastly, you can use submit buttons’ formaction attributes to change the path and query parameters for a form only when a specific submit button is pressed. This one is super handy; I only learned about it while writing this post!
Detecting Which Button Was Pressed
One thing that surprised me when building the new form for ties was that you have no control over what the enter button does; It just submits the first submit button of the form associated to the input. An accessible form is a keyboard-controllable one, so I ended up splitting the bookmark editing form into two, so hitting enter in the list search input would not rename the bookmark.
To know which button a user pressed when submitting a form, you can use a buttons formaction attribute to change the path the form will submit to; Or you can set the name and value attributes of the button to send a specific form value to the server when that button was pressed.
Progressive Enhancement
To support users without JS, I find it easiest to write a whole feature without htmx first. This makes sure I don’t architect myself into a corner where I need to refactor later on when I find out something is not possible without JS.
After that’s done, I sprinkle some htmx on top to improve the experience where JS is available. In my example above, this includes “active search” where users don’t need to hit enter to see search results, and a little spinner to show when the page is waiting for a response from the server. It usually surprises me how little htmx is really needed when I follow this approach.
When testing changes later on, I disable JS using the browsers devtools, or I add a hx-disable attribute.
hx-target
I’m still figuring out the right way to scope htmx swaps. I’ve often found bugs because a too narrow part of the page was updated, leaving other parts of the page showing stale data. Fiddling with out-of-band swaps to solve this feels error prone and overly hard to maintain.
I usually end up swapping the whole page as it’s least likely to break in the future. A downside of this is that sometimes user input will get lost during the swap, but that’s rare and usually acceptable.
Further Reading
That’s it, I hope you took away something useful from this post! If you want find out whether this approach might be suitable for your next project, or if you are already using html/htmx and would like to learn more, here are some of my favorite resources.
Alexander Petros’ blog “Unplanned Obsolescence” is a goldmine, with Less htmx is more and Who’s Afraid of a Hard Page Load? being good starting points.
How I use HTMX with Go and Django + htmx patterns go into some patterns for using htmx in the backend. They translate well to other languages and ecosystems.
If not React, then what? can help you decide if server-rendered html and/or htmx is a good fit for the project you’re building.
Resilient Web Design is an incredibly well-written book about designing robust websites. It’s more about design in the holistic, rather than the visual sense, and has influenced lots of my technological choices. If you read one thing from this list, make it this.
Plain Vanilla Web is a reference for modern capabilities of the browser you might not be aware of, presented as a cookbook for common use-cases.