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.
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.
Loading comments…