django-htmx-nav
A lightweight helper library for Django + HTMX that keeps navigation UI synchronized with the URL, preventing stale sidebars, breadcrumbs, titles, and other surrounding elements.
Homepage · Documentation · Interactive Demo
The Problem
When an HTMX request updates a single container (like #content), regions outside that container, such as active sidebar items, breadcrumb trails, and badge counts, do not update automatically. The main content updates, but the surrounding navigation UI still reflects the previous route:
┌─────────────────────────────────────────────────────────────┐
│ Header / Breadcrumbs (Stale: Page 1) │
├──────────────┬──────────────────────────────────────────────┤
│ Sidebar │ Main Content (Updated: Page 2) │
│ (Stale) │ Targeted element swapped; surrounding │
│ │ navigation controls did not. │
└──────────────┴──────────────────────────────────────────────┘
django-htmx-nav keeps these regions synchronized by making the URL the source of truth for navigation state regardless of whether it is an HTMX request or a full-page load.
Installation
pip install django-htmx-nav
How It Works
1. Automatic Partial Selection (render_nav)
Use render_nav as a drop-in replacement for Django's render(). Direct visits and full-page reloads render the complete template, while HTMX requests return the matching Django partial:
# views.py
from htmx_nav import render_nav
def project_list(request):
projects = Project.objects.all()
return render_nav(
request,
"projects/list.html",
{"projects": projects},
)
<!-- templates/projects/list.html -->
{% extends "base.html" %}
{% block content %}
{% partialdef content inline %}
<div id="content">
{% for project in projects %}
<div>{{ project.name }}</div>
{% endfor %}
</div>
{% endpartialdef %}
{% endblock %}
2. Synchronizing Navigation UI (Swap)
Attach out-of-band swaps to update persistent navigation regions alongside the main response:
# views.py
from django.shortcuts import get_object_or_404
from htmx_nav import Swap, render_nav
from .models import Project
def project_detail(request, pk):
project = get_object_or_404(Project, pk=pk)
return render_nav(
request,
"projects/detail.html",
{"project": project},
swaps=[
Swap(
"components/_sidebar.html",
{"active": project.id},
target_id="sidebar",
),
Swap(
"components/_breadcrumbs.html",
target_id="breadcrumbs",
),
],
title=project.name,
)
The title argument updates the document title during an HTMX navigation while also providing context["title"] during a full-page render.
3. Reusable Navigation Shells (make_shell_renderer)
Define recurring navigation updates once and reuse them across views:
# shells.py
from htmx_nav import Swap, make_shell_renderer
render_shell = make_shell_renderer(
lambda request: [
Swap("components/_sidebar.html", target_id="sidebar"),
Swap("components/_breadcrumbs.html", target_id="breadcrumbs"),
]
)
# views.py
def ticket_detail(request, pk):
ticket = get_object_or_404(Ticket, pk=pk)
return render_shell(
request,
"tickets/detail.html",
{"ticket": ticket},
extra_swaps=Swap(
"components/_status_badge.html",
target_id="ticket-status",
),
)
The shell renderer automatically includes the configured swaps whenever the view is rendered through it. extra_swaps lets individual views add their own out-of-band updates.
4. Class-Based Views
The same navigation shell can be reused with Django's generic class-based views through make_shell_view_mixin:
from django.views.generic import DetailView
from htmx_nav import Swap, make_shell_view_mixin
# Reuse the shell renderer defined above.
ShellViewMixin = make_shell_view_mixin(render_shell)
class ProjectBoardView(ShellViewMixin, DetailView):
model = Project
template_name = "pages/board.html"
context_object_name = "project"
def get_extra_swaps(self):
return Swap(
"components/_status_badge.html",
target_id="project-status",
)
Learn More
- Quickstart Guide — Install the library and build your first URL-driven HTMX navigation flow.
- Architectural Guide — Compare approaches to stale navigation and explore the decision tree for choosing an architecture.
- Benchmark Experiments — Reproduce the benchmark comparing 8 implementation strategies, including MPA, vanilla HTMX, and
django-htmx-nav, or explore the Dashboard for live reference results.
License
Released under the MIT License.
Metadata
Release files for django-htmx-nav 0.3.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_htmx_nav-0.3.3.tar.gz | 31.3 kB | With provenance | Details for django_htmx_nav-0.3.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_htmx_nav-0.3.3-py3-none-any.whl | Python 3 | none | any | Details for django_htmx_nav-0.3.3-py3-none-any.whl |
Total release size: 53.8 kB
Security for django-htmx-nav 0.3.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.