Visual Swap Debugging¶
HTMX_NAV_DEBUG_SWAPS highlights DOM elements as they’re updated by out-of-band HTMX swaps, useful for seeing at a glance what actually changed.
Enable¶
# settings.py
INSTALLED_APPS = [
...,
"htmx_nav",
...,
] # needed for static files + {% htmx_nav_debug_marker %}
# settings.py
HTMX_NAV_DEBUG_SWAPS = True
Two stylesheets are provided, depending on whether you want a client-side on/off toggle:
Toggleable — highlighting only shows while data-hn-debug-swaps is present on <body>. Useful since HTMX_NAV_DEBUG_SWAPS is a server-side setting that needs a reload to flip, but the body attribute can be toggled instantly client-side (e.g. a dev-only button running document.body.toggleAttribute('data-hn-debug-swaps')):
{% load static %}
<head>
<!-- your head -->
<link rel="stylesheet" href="{% static 'htmx_nav/debug-swaps.css' %}">
</head>
<body data-hn-debug-swaps>
<!-- your body -->
</body>
Always-on — highlighting fires any time a swap happens while HTMX_NAV_DEBUG_SWAPS is enabled, no body attribute needed:
{% load static %}
<link rel="stylesheet" href="{% static 'htmx_nav/debug-swaps-always-on.css' %}">
How it works¶
With the setting on, any Swap(target_id=...) appends a script that toggles a .hn-swap class on that element (with a forced reflow, so repeated swaps retrigger it):
<script>
(function(){
var el = document.getElementById("TARGET_ID");
if (!el) return;
el.classList.remove('hn-swap'); void el.offsetWidth; el.classList.add('hn-swap');
})();
</script>
debug-swaps.css flashes the background of any .hn-swap element:
[data-hn-debug-swaps] .hn-swap {
animation: hn-flash-pulse 900ms ease-out;
}
@keyframes hn-flash-pulse {
from { background-color: #fef08a; }
to { background-color: transparent; }
}
Two requirements for the flash to actually show up¶
A stable id on a persistent element. The script runs getElementById after the swap lands, so target_id must be on a node that already exists in the page and keeps the same id every render. Put it on the wrapper in your base/shell template, not inside the swapped partial:
<!-- base.html -->
<div id="sidebar">{% include "components/_sidebar_menu.html" %}</div>
Swap("components/_sidebar_menu.html", context, target_id="sidebar")
An id that only exists inside the fragment, or that changes per render (id="sidebar-{{ project.id }}"), means getElementById finds nothing, or a stale node.
No opaque background covering the target. The flash paints on the element target_id resolves to. If a child fully covers it with its own solid background, the flash renders underneath and is invisible:
<!-- Hidden: <ul>'s own bg-base-200 covers #sidebar -->
<div id="sidebar"><ul class="menu bg-base-200 w-64 p-4">...</ul></div>
<!-- Visible: background lives on the target itself -->
<div id="sidebar" class="bg-base-200"><ul class="menu w-64 p-4">...</ul></div>
Manual OOB wrapping¶
target_id bundles two things: which id the marker flashes, and telling Swap to auto-wrap your fragment in <div hx-swap-oob>. Writing the OOB wrapper yourself (e.g. via a #oob partial, for swap styles or attributes wrap doesn’t cover) opts out of both, Swap never sees your markup to inject a marker into.
Use {% htmx_nav_debug_marker %} instead, placed inside the wrapper (same wrapper-stripping reason as above), with the same id string:
{% load htmx_nav %}
{% partialdef oob %}
<div id="breadcrumbs" hx-swap-oob="innerHTML">
{% partial breadcrumbs %}
{% htmx_nav_debug_marker "breadcrumbs" %}
</div>
{% endpartialdef %}
It respects HTMX_NAV_DEBUG_SWAPS the same way (empty when disabled) and is the same script Swap would’ve emitted, one implementation either way.