The Worst Htmx: A 40-Line Clone That Teaches Core Concepts
Serge Zaitsev's latest post, "Let's make the worst Htmx," is a masterclass in minimalism. He builds a functional htmx clone in 40 lines of JavaScript, stripping away the bloat to reveal the core mechanics. The result is a tiny library that handles AJAX requests, DOM swapping, and event-driven plugins, all without a build step or dependencies.
The Essence: scan, send, swap
Zaitsev identifies htmx's essence as scan, send, swap. The scan() function finds elements with declarative attributes like x-get, x-post, or x-put. It binds event listeners based on the x-trigger attribute (defaulting to click for buttons, submit for forms, and change for inputs). When triggered, the send() function fetches the URL, processes the response, and swap()s the content into the target element.
The initial implementation is remarkably short:
const attr = (el, name) => el.closest(`[${name}]`)?.getAttribute(name);
const METHODS = ['get', 'post', 'put', 'patch', 'delete'];
const defaultTrigger = el =>
el.matches('form') ? 'submit' : el.matches('input,select,textarea') ? 'change' : 'click';
const scan = (root = document.body) =>
METHODS.forEach(m =>
root.querySelectorAll(`[x-${m}]`).forEach(el => {
if (el.$hx) return;
el.$hx = true;
const evt = attr(el, 'x-trigger') || defaultTrigger(el);
el.addEventListener(evt, e => {
e.preventDefault();
send(el, m, el.getAttribute(`x-${m}`));
});
})
);
This core loop is surprisingly powerful. It supports all HTTP methods, custom triggers, and swapping strategies. A MutationObserver efficiently rescans only newly added DOM nodes, avoiding full re-scans.
Swapping Strategies
Zaitsev's swap() function supports multiple modes: outerHTML, beforebegin, afterbegin, beforeend, afterend, delete, and none. This mirrors htmx's hx-swap attribute, giving developers fine-grained control over how response content is inserted.
const SWAP = {
outerHTML: (t, f) => t.replaceWith(f),
beforebegin: (t, f) => t.before(f),
afterbegin: (t, f) => t.prepend(f),
beforeend: (t, f) => t.append(f),
afterend: (t, f) => t.after(f),
delete: t => t.remove(),
none: () => {},
};
Better Triggers and Targets
Zaitsev extends the basic trigger syntax to support comma-separated events with modifiers like delay, changed, and once. He also implements a resolve() function for targets that goes beyond simple querySelector, handling keywords like this, next, previous, document, body, window, and closest/find prefixes. This enables expressions like x-target="closest .container".
Event System and Server Headers
The clone emits four custom events: x:beforeSend, x:afterSend, x:beforeSwap, and x:afterSwap. These events can be intercepted and modified, allowing plugins to alter the request or swap flow. The send() function also respects standard htmx response headers: HX-Trigger, HX-Redirect, HX-Refresh, HX-Retarget, and HX-Reswap.
Plugin Architecture
Zaitsev emphasizes that the core remains tiny because features are added via plugins. He lists several possible plugins, each a dozen lines of code:
x-confirm: nativeconfirm()dialog before sending.x-indicator: shows a spinner during requests.x-disable: disables the element during requests.x-headers: adds custom request headers from a JSON attribute.x-vals/x-include: injects extra values into the request body.x-select: swaps a fragment of the response instead of the whole HTML.x-sync: ensures only one request in flight per target.x-validate: runs browser native constraint validation.x-push-url/x-replace-url: updates browser history.x-sse/x-ws: uses streaming messages.
These plugins hook into the event system without touching the core loop. For example, a x-confirm plugin would listen for x:beforeSend and call preventDefault() if the user cancels.
The Takeaway
Zaitsev's exercise is not about shipping a production-ready library. It's about understanding what makes htmx tick. By building the worst clone possible, he demystifies the magic and shows that the core concept is simple. The real value lies in the plugin architecture, which allows developers to extend the library without bloating the core.
If you've ever been curious about how htmx works under the hood, this post is a perfect starting point. You'll come away with a deeper appreciation for the elegance of declarative AJAX and the power of a well-designed event system.