Skip to main content

Stale Navigation in HTMX & django-htmx-nav

A reference repository and lightweight helper library exploring solutions for stale navigation regions in HTMX-driven Django applications.

The Problem

When an HTMX request updates a single target container (like #main-content), regions outside that container, such as active sidebar items, breadcrumb trails, tab indicators, or multi-step progress bars, do not update automatically.

This creates UI state drift: the main content updates, but surrounding navigation elements still reflect the previous route. HTMX targets and swaps single elements by default, leaving multi-region layout updates to the developer.

┌─────────────────────────────────────────────────────────────┐
│ Header / Breadcrumbs (Stale: Page 1)                        │
├──────────────┬──────────────────────────────────────────────┤
│              │                                              │
│ Sidebar      │  Main Content (Updated: Page 2)              │
│ (Stale:      │                                              │
│  Item 1)     │  Targeted element swapped successfully.      │
│              │  Surrounding navigation controls did not.    │
│              │                                              │
└──────────────┴──────────────────────────────────────────────┘

Solutions & Benchmark Comparison

There is no single "correct" way to handle multi-region updates. The right approach depends on application complexity, payload constraints, and developer ergonomics.

The accompanying example project and benchmark suite evaluate 8 distinct strategies across the same demo application:

Category Method / Strategy Live Demo Route Description
Baseline Plain MPA /mpa/ Standard multi-page app with full reloads.
Vanilla HTMX hx-boost Only /htmx/ Full HTML shells returned on every boosted request.
Vanilla HTMX Hand-Written OOB (Partial) /vanilla-htmx/composite/ Views manually build hx-swap-oob fragments for core regions.
Vanilla HTMX Hand-Written OOB (Full) /vanilla-htmx/atomic/ Explicit OOB updates for all regions with template branching.
Package Baseline Shell /htmx-nav/baseline/ Static shell rendering using make_shell_renderer.
Package Per-View Swaps /htmx-nav/composite/ Dynamic Swap lists attached per view.
Package Explicit Atomic /htmx-nav/atomic/ Granular per-region swaps attached in views.
Package Declarative Registry /htmx-nav/declarative/ Route-aware central registry resolving swaps automatically.

Client-Side Variants: Many HTMX strategy in the demo can also be toggled to evaluate hx-select (extracting regions client-side) and Idiomorph (DOM morphing instead of inner/outer HTML swapping).

Benchmark Highlights

Key observations from reference benchmark runs:

  • Payload Savings: Partial updates reduce on-wire transfer size by ~32% compared to full-page reloads, whether implemented via vanilla OOB fragments or django-htmx-nav.
  • Server Overhead: Building multi-region OOB updates via django-htmx-nav adds <0.5 ms of server-side rendering time over base views.
  • Database Performance: Request-scoped caching (cache_on_request) keeps database query counts flat (~4.5 avg) regardless of how many OOB fragments are generated per request.

What is django-htmx-nav?

django-htmx-nav is an optional, lightweight Python helper designed to streamline out-of-band (OOB) swap construction and partial resolution in Django views without forcing a rigid architecture.

pip install django-htmx-nav

1. Basic Partial Rendering (render_nav)

Native Django partials allow a single template to serve both full-page requests and partial HTMX swaps:

<!-- templates/app/project_list.html -->
{% extends 'base.html' %}

{% block content %}
{% partialdef content inline %}
  {% for project in projects %}
    <div>{{ project.name }}</div>
  {% endfor %}
{% endpartialdef %}
{% endblock %}

render_nav inspects incoming headers to render either the full shell (on direct loads) or the isolated content partial (on HTMX requests), while setting appropriate Vary: HX-Request headers:

from htmx_nav import render_nav

def project_list(request):
    return render_nav(
        request, 
        "app/project_list.html", 
        {"projects": Project.objects.all()}
    )

2. Manual OOB Swaps (Swap)

When a sub-region (such as #content) changes, you can append specific OOB swaps for surrounding navigation elements:

from htmx_nav import Swap, render_nav

def project_detail(request, pk):
    project = get_object_or_404(Project, pk=pk)
    return render_nav(
        request,
        "app/project_detail.html",
        {"project": project},
        swaps=[
            Swap("app/_sidebar.html", {"active_pk": project.pk}, target_id="sidebar"),
            Swap("app/_breadcrumbs.html", {"project": project}, target_id="breadcrumbs"),
        ],
    )

3. Reusable Shell Rendering (make_shell_renderer)

To avoid repeating OOB swap lists across every view, make_shell_renderer abstracts common shell and navigation context into a reusable render function:

from htmx_nav import make_shell_renderer

render_shell = make_shell_renderer(
    shell_template="app/_shell.html",
    context_builder=lambda request: {"nav": build_nav_context(request)},
)

def project_detail(request, pk):
    project = get_object_or_404(Project, pk=pk)
    # Automatically attaches shell-level OOB swaps derived from build_nav_context
    return render_shell(request, "app/project_detail.html", {"project": project})

4. Target-Aware Resolution (targeting)

Views can dynamically adjust rendered partials based on the incoming HX-Target header:

from htmx_nav import Swap, make_shell_renderer, targeting

render_project = make_shell_renderer(
    "app/_shell.html",
    context_builder=lambda request: {"nav": build_nav_context(request)},
)

def project_tab(request, pk):
    project = get_object_or_404(Project, pk=pk)
    return render_project(
        request,
        "app/project_detail.html",
        {"project": project},
        partial={
            "#tab_content": targeting("tab-content"),
            "#main_content": targeting("main-content"),
            "#content": True,
        },
        extra_swaps=[
            Swap("app/_tabs.html", {"active": "overview"}, target_id="tabs")
        ],
    )

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_htmx_nav-0.3.1.tar.gz (32.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_htmx_nav-0.3.1-py3-none-any.whl (24.2 kB view details)

Uploaded Python 3

File details

Details for the file django_htmx_nav-0.3.1.tar.gz.

File metadata

  • Download URL: django_htmx_nav-0.3.1.tar.gz
  • Upload date:
  • Size: 32.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for django_htmx_nav-0.3.1.tar.gz
Algorithm Hash digest
SHA256 d182825f8cf4c77126232604396d65469d0a161743ae5f3c07e7a5d48da56c87
MD5 a7bfe1fcbf1edb69152c06ce414551eb
BLAKE2b-256 da6993574f1d6ef75ff7775a5d6aacb3e20e993ac644d2eae41a0b01f4c879b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_htmx_nav-0.3.1.tar.gz:

Publisher: python-publish.yml on lucas-rollin/django-htmx-nav

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_htmx_nav-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: django_htmx_nav-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 24.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for django_htmx_nav-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5bd8e8dedde75a06e779291fde45ec91cb3a9d2318575038bd8c871a0cd106e0
MD5 c1e768afcf692886ad86611370ed8a8d
BLAKE2b-256 a41ce8cc39b2d8c74138b6f1ed7052f5be157566a5c2fccd06a84262cb053ef4

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_htmx_nav-0.3.1-py3-none-any.whl:

Publisher: python-publish.yml on lucas-rollin/django-htmx-nav

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page