Imported from yardenshoham/onepiece (
.agents/skills/htmx-debugging/SKILL.md). Install upstream withnpx skills add yardenshoham/onepiece --skill htmx-debugging. Copyright stays with the author.
htmx 4 Debugging Guide
Quick Diagnostic Checklist
Work through these in order -- most issues fall in the first few:
- Is htmx loaded? Check for
htmxglobal in console, verify script tag - Is the element processed? Check
element._htmxproperty exists - What htmx version? Check
htmx.version-- the htmx 4 API is very different from htmx 2 - Is the trigger correct? Defaults:
clickfor most,changefor inputs/selects/textareas,submitfor forms - Is the target correct? Verify the CSS selector matches an existing element
- Does the server return HTML? Not JSON -- check Content-Type and response body
- Check the response status -- htmx 4 swaps ALL responses except 204 and 304 by default
- Is inheritance set up? htmx 4 requires
:inheritedmodifier on parent attributes - For extensions: Is the script loaded after
htmx.js? If you set theextensionswhitelist, the registration name must be in it. An unset whitelist allows every extension
Enable Debug Logging
Errors and warnings flow to console.error / console.warn by default. To also surface every event htmx dispatches:
<meta name="htmx-config" content='{"logAll": true}'>
or in the console:
htmx.config.logAll = true;
Observability tools (Sentry, DataDog RUM, LogRocket, etc.) capture console.* automatically, so htmx logs flow into your existing pipeline without any extra setup.
Event Monitoring Snippet
Paste this in the console to monitor the request/swap lifecycle:
['htmx:config:request', 'htmx:before:request', 'htmx:after:request',
'htmx:before:swap', 'htmx:after:swap', 'htmx:finally:swap', 'htmx:error', 'htmx:finally:request']
.forEach(evt => document.body.addEventListener(evt, e => {
console.log(evt, e.detail?.ctx?.request?.action, e.detail?.ctx?.response?.status, e.detail);
}));
To monitor what events a specific element is firing:
monitorEvents(htmx.find("#theElement"));
Common Issues and Solutions
Request Not Firing
Check the trigger:
- Is the event actually happening? Use
monitorEvents()on the element - Default triggers differ by element type -- an
<input>won't fire onclick - If using
hx-trigger="load", was the element in the DOM before htmx initialized?
Check synchronization:
hx-syncmay be dropping or queuing the request- Check for
hx-sync="closest form"or similar that might block it
Check confirmation:
hx-confirmblocks until confirmed -- includingjs:async confirmation- An
htmx:confirmevent listener callingpreventDefault()without callingissueRequest()will block forever
Check for hx-ignore:
- A parent element with
hx-ignoredisables htmx for all children
Dynamic content:
- Elements added to the DOM after page load need
htmx.process(element)to initialize htmx behavior - Or use
htmx.onLoad()to set up a callback for new content
Swap Not Happening
Check response status:
204and304do NOT swap by default (controlled byhtmx.config.noSwap)- In htmx 4, 4xx and 5xx responses DO swap by default (unlike htmx 2!)
- If you need htmx 2 behavior:
htmx.config.noSwap = [204, 304, '4xx', '5xx']
Check the target:
- Does the
hx-targetCSS selector match an existing element? - Use browser devtools to run
document.querySelector("your-selector")to verify
Check hx-swap:
hx-swap="none"explicitly prevents swappinghx-swap="delete"deletes the target regardless of response
Check hx-select:
- If set, only matching elements from the response are used
- If nothing matches, nothing gets swapped
Check event listeners:
- An
htmx:before:swaplistener callingpreventDefault()will cancel the swap
Wrong Content Being Swapped
Check selectors:
hx-selectmight be matching the wrong element in the responsehx-targetmight point to the wrong element
Check for OOB/partial interference:
hx-swap-oobin the response swaps content by ID independently<hx-partial>tags in the response swap into their own targets- In htmx 4, OOB swaps happen AFTER the main content swap (changed from htmx 2)
Check response headers:
HX-Retarget,HX-Reswap,HX-Reselectoverride client-side attributes, and htmx applies them beforehx-statushx-status:XXXattributes can change target, swap, select or history handling for one status code. htmx tries the exact code first, thenNNx, thenNxx, and stops at the first match
Extension Not Working
- Is the extension script loaded AFTER htmx.js?
- If you set the
extensionswhitelist, is the extension name in it? An unset whitelist allows every extension - Does the name match exactly? It is case-sensitive, and the registration name is not always the file name.
hx-sse.jsregisters assse,hx-preload.jsaspreload,htmx-2-compat.jsascompat - Check console for registration errors
- htmx 4 extensions use
htmx.registerExtension()nothtmx.defineExtension()-- make sure you have an htmx 4 compatible extension
Inheritance Not Working
The #1 gotcha in htmx 4:
<!-- WRONG: children won't inherit this -->
<div hx-target="#output">
<button hx-get="/a">A</button>
</div>
<!-- RIGHT: use :inherited modifier -->
<div hx-target:inherited="#output">
<button hx-get="/a">A</button>
</div>
- htmx 4 requires
:inheritedmodifier by default - Set
htmx.config.implicitInheritance = trueto get htmx 2 behavior - Check that it's on the PARENT, not the child
History/URL Issues
hx-push-urlandhx-replace-urlrequire the URL to return a full page when accessed directly- History restoration in htmx 4 does a full page request (no localStorage/sessionStorage cache)
- Set
htmx.config.history = "reload"to do hard browser reloads on back/forward hx-statusattributes withpush:falsecan prevent URL updates on errors
CSS Transitions Not Working
- CSS transitions rely on element ID stability across swaps -- keep
idattributes consistent htmx-swappingclass is applied before swap,htmx-settlingafter- For View Transitions API: enable with
htmx.config.transitions = trueorhx-swap="... transition:true" - Morphing (
innerMorph/outerMorph) preserves animations better thaninnerHTML/outerHTML
Form Data Not Included
GETandDELETErequests do NOT include enclosing form data by default in htmx 4- Fix: add
hx-include="closest form"to include form values - Non-GET/DELETE requests (POST, PUT, PATCH) DO include enclosing form values automatically
- Check
hx-valssyntax: it takes HCON (key:value, other:2), which also accepts JSON. Use thejs:prefix for dynamic values
htmx 2 Code Not Working in htmx 4
Quick compatibility fixes:
- Add
htmx.config.implicitInheritance = true(restores automatic inheritance) - Add
htmx.config.noSwap = [204, 304, '4xx', '5xx'](restores htmx 2 swap behavior) - Replace
hx-ext="name"with<script src="ext.js">. Theextensionsconfig is an optional whitelist, not a requirement - Update event names:
htmx:beforeRequest->htmx:before:request,htmx:afterSwap->htmx:after:swap, etc. - Replace
hx-disabled-elt->hx-disable - Replace
hx-disable(old meaning of ignoring) ->hx-ignore - Replace
hx-vars->hx-valswithjs:prefix - Load the
hx-promptextension to keephx-promptworking - Or load the
htmx-2-compatextension for gradual migration
Browser DevTools Techniques
Network Tab
- Filter by Fetch/XHR requests
- Look for
HX-Request: truein request headers to confirm htmx is making the request - Check response headers for
HX-Trigger,HX-Retarget,HX-Reswap - Check response body -- should be HTML, not JSON
Elements Panel
- Inspect element and check for
_htmxproperty (indicates htmx processed it) - Look for
htmx-requestclass during active requests - Look for
htmx-swapping/htmx-settlingclasses during swaps
Console
htmx.config.logAll = true-- log every event (errors and warnings already on by default)htmx.version-- confirm which major version is loadedhtmx.find("#selector")-- test extended CSS selectorshtmx.trigger(elt, "eventName")-- manually fire events
Instructions for Claude
When helping users debug htmx issues:
- Ask about the htmx version first -- htmx 2 vs 4 is the most common source of confusion
- Suggest
htmx.config.logAll = trueas the first step and ask for console output (errors/warnings are visible without it) - Check for
:inheritedmodifier when inheritance problems are reported - Check the Network tab -- verify the request is being made and inspect the response
- Check the response body -- it should be HTML, not JSON
- Look for typos in attribute names (e.g.
hx-trigerinstead ofhx-trigger) - Check
hx-statusattributes that might override swap behavior for specific status codes - Verify the server returns appropriate status codes (200 for success, 422 for validation errors, 204 for no-content)
