Fix: Empty class="" attribute left behind after htmx operations

Issue #1701 · Supersedes PR #2683 · Base: 1e807809 (dev)

Before Fix

upstream/dev @ 1e807809

After htmx removes its settling/swapping/request classes, elements with no other classes are left with an empty class="" attribute.

Click the buttons above…

After Fix

1e807809 + fix

Uses removeClassFromElement() which already cleans up empty class attributes. Three call sites fixed.

Click the buttons above…

The Bug

htmx already has removeClassFromElement() which checks classList.length === 0 and calls removeAttribute('class'). But three internal sites use raw classList.remove() instead, leaving behind class="" on elements that had no classes before the htmx operation. While benign, this means the post-swap DOM doesn't match a fresh server render — breaking snapshot tests and DOM comparison tools.

Test 1 (swap) exercises the swappingClass and settlingClass removal paths. Test 2 (indicator) exercises the requestClass removal path — htmx adds htmx-request to elements referenced by hx-indicator during a request, then removes it when the response arrives. Both go through htmx's real internal request lifecycle via a mock XMLHttpRequest.

// doSettle — settlingClass removal
- elt.classList.remove(htmx.config.settlingClass)
+ removeClassFromElement(elt, htmx.config.settlingClass)
// swap — swappingClass removal
- target.classList.remove(htmx.config.swappingClass)
+ removeClassFromElement(target, htmx.config.swappingClass)
// indicators — requestClass removal
- ic.classList.remove.call(ic.classList, htmx.config.requestClass)
+ removeClassFromElement(ic, htmx.config.requestClass)
Both panels load htmx from the same base commit (1e807809). A mock XMLHttpRequest intercepts network calls so htmx runs its full request lifecycle (add indicator class → swap response → settle → remove classes) without needing a server. The left panel shows class="" left behind; the right panel removes it cleanly.