htmx: Build Interactive Web Apps with HTML · Beyond the Basics
htmx and JavaScript Together
Use hx-on for small behaviours, react to htmx events, initialise widgets in swapped content, and pair htmx with Alpine.js.
What you'll build
A contact card list where each new card (loaded by htmx) gets a "copy email" button initialised by JavaScript, and a small client-side toggle that needs no server.
The idea
htmx handles talking to the server; some behaviour is purely client-side. Use the lightest tool:
- Plain HTML first:
<details>,<dialog>,popover. hx-on:<event>for one-liners (hx-on:click,hx-on::after-request).htmx.onLoad(fn)(thehtmx:loadevent) to initialise widgets in any content htmx adds, including the initial page. Initialising only onDOMContentLoadedmisses swapped content.- Alpine.js for small reactive state in the markup (
x-data,x-show); it works well alongside htmx. htmx.process(el)when your own code inserts HTML withhx-attributes.
The code
<div id="cards"></div>
<button hx-get="/cards" hx-target="#cards" hx-swap="beforeend">Load contacts</button>
<details><summary>Help (plain HTML, no request)</summary><p>Click "Copy" to copy an email address.</p></details>
<script>
// Runs for the initial page and for every piece of content htmx swaps in.
htmx.onLoad((root) => {
root.querySelectorAll("[data-copy]").forEach((btn) => {
if (btn.dataset.ready) return;
btn.dataset.ready = "1";
btn.addEventListener("click", () => { btn.textContent = "Copied " + btn.dataset.copy; });
});
});
</script>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
let n = 0;
const people = ["ada", "grace", "alan", "katherine"];
server.get("/cards", () => {
const name = people[n++ % people.length];
return `<p>${name}@example.com <button data-copy="${name}@example.com">Copy</button></p>`;
});
</script>
With Alpine.js, client-only state stays in the markup:
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<div x-data="{ open: false }">
<button @click="open = !open">Filters</button>
<form x-show="open" hx-get="/contacts" hx-target="#rows">...</form>
</div>
Common mistakes
- Initialising widgets on
DOMContentLoadedonly: content added later by htmx stays dead. Usehtmx.onLoad. - Binding the same listener twice when content is processed again; mark initialised elements.
- Using
hx-onwith camelCase event names: HTML attribute names are lower-case, so use kebab-case events. - Making a server round trip for purely visual state (open/closed menus).
Key points
- Use HTML, then
hx-on, then Alpine or small scripts for client behaviour. htmx.onLoadinitialises widgets in all new content.htmx.processactivates HTML your own code inserts.
Further reading: hx-on · htmx JavaScript API · htmx docs: scripting
Practice questions
Why should widget initialisation use htmx.onLoad instead of DOMContentLoaded?
Answer
DOMContentLoaded runs once; htmx.onLoad runs for the initial page and every piece of content htmx swaps in later.
When do you need htmx.process()?
Answer
When your own JavaScript inserts HTML containing hx- attributes; htmx only processes content it swaps itself.
Why use kebab-case event names with hx-on?
Answer
HTML attribute names are case-insensitive and lower-cased, so hx-on:myEvent would listen for 'myevent'.
Open in the app to mark lessons complete, take notes and track your progress.