November 11, 2025 Permalink
Hot take: the killer feature of htmx which transforms it from a nice lil’ opinionated DSL for HTML-based reactivity to a powerhouse full-stack framework with which you can build ambitious, even experimental application architectures is this:
Unfortunately, if you follow the link above to “Building htmx extensions”, there’s virtually no documentation there. 😒
So I’m guessing the answer to Why didn’t anyone tell me about this sooner?? is likely Because nobody knows!
I’m here to rectify that situation; in particular, explain the ability for you to build your own custom swaps.
What is a Custom …
November 11, 2025 Permalink
Hot take: the killer feature of htmx which transforms it from a nice lil’ opinionated DSL for HTML-based reactivity to a powerhouse full-stack framework with which you can build ambitious, even experimental application architectures is this:
Unfortunately, if you follow the link above to “Building htmx extensions”, there’s virtually no documentation there. 😒
So I’m guessing the answer to Why didn’t anyone tell me about this sooner?? is likely Because nobody knows!
I’m here to rectify that situation; in particular, explain the ability for you to build your own custom swaps.
What is a Custom Swap? (for that matter, what is a swap?!)
In htmx in a nutshell, the following example is provided:
<button
hx-post="/clicked"
hx-target="#parent-div"
hx-swap="outerHTML"
>
Click Me!
</button>
In UX-speak, this translates to: “As a user, when I click the Click Me! button, I would like to see the new content appear in the box” (the “box” being a <div id="parent-div"> element somewhere on the page, and “content” being an HTML fragment returned by the server).
htmx comes with a number of swap mechanisms at your disposal, including ones like beforeend which is equivalent to targetEl.append(anotherEl) and even delete which ignores a response and simply deletes a target element.
Let’s call these built-in swaps. You can certainly accomplish a lot with only the built-in swaps, as evidenced by the growing popularity of htmx. However this leads us to the question at hand: Can I create my own swap?
Yes!
The genius of htmx is that the hx-swap attribute can be anything you want. hx-swap="bop"? Sure. hx-swap="snap-crackle-pop" Absolutely!
Yet the behavior you’ll end up with out-of-the-box is the same as innerHTML because you haven’t defined what these new swap mechanisms are. That’s where extensions come in.
Let’s write the most simple extension imaginable. Assuming there’s an htmx import or global defined, in your JavaScript entrypoint write:
htmx.defineExtension("bop", {
isInlineSwap: (swapStyle) => swapStyle === "bop",
handleSwap: (swapStyle, _target, /** @type {Element} */ fragment) => {
if (swapStyle === "bop") {
console.log("Bop!", fragment) // log the server HTML fragment
return true
}
}
})
You’ll also need to add an hx-ext attribute on your site layout’s body:
<body hx-ext="bop">
Now you can update the button example from above!
<button hx-post="/whoop" hx-swap="bop">Boop!</button>
Clicking Boop! will go through the normal htmx ritual of calling the server and parsing the incoming HTML into a DOM fragment. However this time, your custom swap will kick in and you’ll get a Bop! console log. 🎉
Let’s talk about boosting for a moment. htmx provides an option where forms automatically submit using the AJAX technique so you can easily use swapping techniques. You can boost via the hx-boost attribute either on a container of your page or on <body> itself.
However, once you do this, your browser URL will change when htmx handles a request/response thus adding to your history. If you don’t want your custom swap to affect browser history and you don’t want to require users to fiddle with htmx’s history attribute, you can do so manually. Let’s add an event handler to our extension:
onEvent: (name, event) => {
if (name !== "htmx:beforeOnLoad") return
/** @type {HTMLElement} */
const elt = event.detail.elt
if (elt.getAttribute("hx-swap") === "bop" && !elt.hasAttribute("hx-push-url")) {
elt.setAttribute("hx-push-url", "false")
}
},
This sets the hx-push-url manually if it’s not already present on the element triggering the swap, thus indicating to htmx it shouldn’t mess with browser history.
And…that’s all folks! If you’ve ever looked at a library like htmx and wondered but what if I want to write my own code to deal with an incoming HTML fragment? (or am I the only weirdo who does this?! 😅), this is your solution.
Have a Piece of Toast
It wouldn’t be a fresh That HTML Blog installment without a demo, so for this one I thought I’d come up with a fun little strategy of returning messages from the backend so the frontend can display a toast notification. Since my fav UI library Web Awesome sadly doesn’t come with toasts yet (unlike its Shoelace predecessor), I roped in the Simple Notify library which is actually pretty stellar.
In a nutshell, my custom toasts htmx extension pulls in HTML like this:
<output class="toast-notification" data-title="Success!">
I'm a toast from the server!
</output>
And turns it into Notify-compatible JavaScript:
new Notify({
status: toast.dataset.status ?? "success",
title: toast.dataset.title,
text: toast.textContent,
effect: "slide",
speed: 400,
type: toast.dataset.type ?? "outline",
position: "right bottom"
})
Could you have come up with your own vanilla JS solution, perhaps fetching some JSON instead and using that to populate the toast values? Sure…but then that wouldn’t be very htmx-y now would it?
The htmx ethos is all about using HTML as the source of truth in your application. HTML fragments can be used verbatim to populate the DOM, but why not use HTML an an interchange format as well? After all, once you introduce custom elements into the mix, you’re practically writing XML using a schema you invent for the task at hand. Is that a bad thing? I would say no! And considering the recent outcry over removing XSLT from browsers, I’m not the only one. 😂
So that’s htmx extensions. What will you create with it? Hop on over to Human Web Collective’s indie_web_dev@humansare.social Forum or Discord to chat about this and a whole lot more!