Charlie KrugThe Build Log

← All posts

The network panel shows a real request. Production has no server.

Pick an hx-* preset on a live htmx element and watch the real request and the flash-highlighted DOM patch land side by side. In production a service worker answers, so the network panel is never a mock.

Flip one attribute on a live button from hx-swap="innerHTML" to hx-swap="outerHTML" and press it. On the left, a network panel prints the actual HTTP call: method, URL, request headers, the response body htmx just received. On the right, the exact DOM nodes that got replaced flash-highlight the instant the swap lands. With innerHTML the button's contents light up. With outerHTML the whole button does. You are not reading a description of the difference. You are watching it happen, twice, in under a second.

That is the entire pitch of Swapscope. htmx is built on one genuinely simple idea: attributes drive behavior. But every resource for learning it is static prose, a table with hx-swap in the left column and "specifies how the response will be swapped in" on the right. That tells you the name of the concept, never the shape of it. You copy an example, run it, and end up reverse-engineering what the browser actually did. Swapscope is the page you can point a teammate at that just shows it: pick a preset, watch the wire, watch the DOM.

Instrumentation, not animation

The tempting way to build this is to fake it: a hardcoded "request" read-out next to a canned animation of nodes moving. That would be another diagram, and diagrams are the problem, not the fix. So nothing here is staged. htmx always issues a real request, and both panels are driven by htmx's own lifecycle events. The tool taps htmx:configRequest to capture the outgoing call, htmx:afterRequest for the real response, and htmx:afterSwap to know precisely which nodes changed, then flashes exactly those.

The consequence of tapping stock htmx instead of forking it is the thing that makes the demo trustworthy: what you see is what any htmx app does, not a behavior special to this page. And because the swap-detection rides on the actual afterSwap event rather than a polling timer, there is no sampling gap, no intermediate state that flickered past between two ticks and got missed. The highlight lands on the same beat as the swap because it is the swap telling you about itself.

One more decision holds the whole thing together: one demo element, many presets, not one page per attribute. Switching innerHTML to outerHTML on the same button is what makes the comparison legible. Separate pages would lose the side-by-side, which is the only reason the distinction ever clicks. There is even a compare mode that fires one trigger against two swap strategies at once, each instrumented independently from the same real event stream.

A real request answered by nothing

Here is the part I like most. The live site at apps.charliekrug.com is static files on a CDN. There is no server. So how does htmx make a real HTTP request to api/demo and get a real fragment back, when nothing is running to answer it?

A service worker. The frontend talks to a small Go server (stdlib net/http, the static/ directory embedded straight into the binary via go:embed) when you run it locally or self-host. In production, static/sw.js intercepts the same api/demo requests in the browser and answers them from a byte-for-byte JavaScript port of the Go renderer. htmx does not know the difference, and neither does the network panel: it is showing a genuine request and a genuine response, produced by the same rendering logic, whether Go or the worker did the producing.

Keeping two independent backends emitting identical bytes is the kind of thing that silently drifts and rots. The fix is a golden manifest test: a recorded set of expected fragments that both the Go handler and the JS worker are checked against, so if either one's output changes by a single byte, CI fails. The credibility of the whole tool rests on that panel not being a mock, and this is what keeps it honest across a deployment that has no runtime at all.

Try it

Open Swapscope, find the hx-swap toggle, and flip it from innerHTML to outerHTML while watching the DOM panel. Then work through the other presets: hx-trigger with click, revealed, and a delay:500ms modifier; hx-target and hx-select; the hx-indicator loading state landing and clearing. Turn on compare mode to run two swap strategies at once. The full preset state lives in the URL, so once you find a demo that explains a subtlety, copy the link and send it. The source is on GitHub, and view-source on the instrumentation is short enough to read in a coffee break.

Swapscope is live. Free, in your browser, no signup.

This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.

Comments

Loading comments…