Interactive Helpdesk Demo & Empirical Benchmark Suite

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.

Default HTMX State Drift

Single-Target Swap (#main-content)

Header / Breadcrumbs: Stale (Page 1)
Sidebar: Stale (Tab 1)
Main Content: Updated (Page 2)

The targeted block updates, but sidebar active states and breadcrumb trails stay stuck on the previous route.

django-htmx-nav Fully Synchronized

Target + Multi-Region OOB Updates

Header / Breadcrumbs: Synced (Page 2)
Sidebar: Synced (Tab 2)
Main Content: Updated (Page 2)

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.

Baseline Full reload

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
Manual HTMX Hand-rolled OOB

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
Recommended
Helper Library Declarative

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
Empirical Data

Benchmark Highlights

Measured across reproducible testbeds with Playwright and server instrumentation.

Open Full Benchmark Suite
~32%
Payload Reduction
Average wire size reduction compared to views unaware vanilla HTMX.
<0.5 ms
Render Overhead
Minimal server-side compute cost for building multi-region OOB swaps.
4.5 avg
Flat DB Queries
Request-scoped caching prevents duplicate queries across partials.
23
Tested Variants
Comprehensive comparison across MPA, Vanilla HTMX, hx-select, and Idiomorph.

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)