Server-Driven Navigation for HTMX in Django
Keep sidebars, breadcrumbs, and active navigation controls synchronized with the URL as the single source of truth without state drift or boilerplate.
pip install django-htmx-nav
The Stale Navigation Problem
When an HTMX request updates only an inner container (like #main-content), surrounding layout controls do not update automatically.
Single-Target Swap (#main-content)
The targeted block updates, but sidebar active states and breadcrumb trails stay stuck on the previous route.
Target + Multi-Region OOB Updates
Out-of-band swaps automatically update surrounding navigation elements in the same HTTP response.
3 Architectural Approaches Compared
This live deployment implements the exact same Helpdesk application across 3 distinct architectural models so you can compare developer experience, code complexity, and wire payload.
Pure Multi-Page App (MPA)
Traditional full-page reloads on every navigation. Simplest conceptual model with zero client-side coordination, but transfers full layout HTML on every click.
- ✓ Native browser navigation, nothing to learn
- ✓ Zero client state to keep in sync
- ✗ ~32% more bytes on the wire per navigation
Vanilla HTMX
Standard HTMX with hand-written out-of-band fragments. Every view checks HX-Target itself and assembles the right partials. Complex swaps may require branching logic inside django templating.
- ✓ Fast partial updates on the wire
- ✗ Verbose template branching and string joining
- ✗ Hard to know pinpoint swaps work properly
django-htmx-nav
One primitive, Swap, wraps your existing, ordinary templates for OOB delivery. render_nav resolves full-page vs. partial for you.
- ✓ Same ~32% payload reduction as hand-written OOB
- ✓ Clean, declarative view code
- ✓ Testing and visual debugger helpers
Benchmark Highlights
Measured across reproducible testbeds with Playwright and server instrumentation.
How It Looks in Your Views
No custom middleware required. Just a concise, expressive wrapper around Django's standard rendering pipeline.
from django.shortcuts import get_object_or_404
from htmx_nav import Swap, render_nav
def project_board(request, org_id, project_id):
project = get_object_or_404(Project, id=project_id)
context = {"project": project, "tickets": project.tickets.all()}
# Full page for direct visits, partial + OOB swaps for HTMX:
return render_nav(
request,
"pages/board.html",
context,
swaps=[
Swap("components/_sidebar.html", target_id="sidebar"),
Swap("components/_breadcrumbs.html", target_id="breadcrumbs"),
],
)
from django.shortcuts import get_object_or_404
from htmx_nav import Swap, make_shell_renderer
# Defined once, reused by every view in the app:
render_shell = make_shell_renderer(lambda request: [
Swap("components/_sidebar.html", sidebar_ctx(request), target_id="sidebar"),
Swap("components/_breadcrumbs.html", crumbs_ctx(request), target_id="breadcrumbs"),
])
def project_board(request, org_id, project_id):
project = get_object_or_404(Project, id=project_id)
context = {"project": project, "tickets": project.tickets.all()}
# Sidebar & breadcrumbs come along for free, every time:
return render_shell(request, "pages/board.html", context)