Skip to main content

django-htmx-nav

Docs

Django 6 added native template partials, so a single file can define both the full page and the fragment HTMX swaps into:

{% extends 'base.html' %}

{% block content %}
{% partialdef content inline %}
  <div>My page!</div>
{% endpartialdef %}
{% endblock %}

That's most of what you need for server-driven, SPA-like UX with MPA simplicity: the URL stays the source of truth, and a full load vs. an HTMX swap render the same fragment. What's still missing is the boilerplate around it: detecting HTMX, picking the partial, and doing HTMX-safe redirects. And, once your page has more than one region (a sidebar, breadcrumbs), keeping those regions from drifting out of sync depending on how the page was reached.

django-htmx-nav provides lightweight helpers for that. Not a Django app, nothing to add to INSTALLED_APPS.

pip install django-htmx-nav

render_nav: partial rendering, done

from htmx_nav import render_nav


def project_list(request):
    return render_nav(
        request, "app/project_list.html", {"projects": Project.objects.all()}
    )
{% extends 'base.html' %}
{% block content %}
{% partialdef content inline %}
  {% for project in projects %}<div>{{ project.name }}</div>{% endfor %}
{% endpartialdef %}
{% endblock %}

Full page load → renders the whole template. HTMX request → renders only the content partial and sets Vary: HX-Request so caches never serve one variant to the other kind of request.

But you likely need to update your sidebar too

A tab click swaps #content, but if the sidebar shows an active-item highlight, or breadcrumbs, those live outside #content and won't update on their own. HTMX's out-of-band swaps solve this: render extra fragments alongside the main one, each targeting its own DOM id.

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": project.pk}, target_id="sidebar"),
            Swap(
                "app/_breadcrumbs.html", {"project": project}, target_id="breadcrumbs"
            ),
        ],
    )

This works, but every view that touches the sidebar now needs to rebuild the same nav context and remember to pass the same Swaps, easy to forget in view #12.

make_shell_renderer: the shell, abstracted away

make_shell_renderer bakes a shell template + its context into a render_shell function, so call sites go back to looking like a plain view — the shell just always comes along for free:

from htmx_nav import make_shell_renderer

render_shell = make_shell_renderer(
    shell_template="app/_shell.html",  # renders sidebar + breadcrumbs together
    context_builder=lambda request: {"nav": build_nav_context(request)},
)


def project_detail(request, pk):
    project = get_object_or_404(Project, pk=pk)
    return render_shell(request, "app/project_detail.html", {"project": project})

Every HTMX response from render_shell includes the shell as an out-of-band swap, built from the same build_nav_context a full page load would use, so the sidebar can never show one thing on first load and another after an HTMX swap. render_shell accepts the same keyword arguments as render_nav (extra_swaps, partial ...), so it's a drop-in.

Target-aware partial resolution

Views can resolve different partial blocks dynamically based on HX-Target headers using target specifications:

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")
        ],
    )

The view stays focused on what's actually tab-specific; partial selection and out-of-band shell delivery are handled cleanly by render_project.


⚠️ This package is young. The API (function signatures, keyword argument names, module layout) may still change between releases.

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.0.tar.gz (25.4 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.0-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_htmx_nav-0.3.0.tar.gz
  • Upload date:
  • Size: 25.4 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.0.tar.gz
Algorithm Hash digest
SHA256 8d4fe2c73ca8b874161582d21b3d59e61c7057ec90807d227890bb4c5cd9c3b3
MD5 dd323e2f0f90ff790f04f0da09b87a69
BLAKE2b-256 466dbb6178b8508f72fdd9c0bb28cb8b7e88421adf9651e0dc20e1f72f79537b

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_htmx_nav-0.3.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: django_htmx_nav-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 18.9 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6f99f7926cd3369e66392f2cc221043958b1304ced5f6b12ebb97c605d1ac70d
MD5 93dfc814b8a5a8b686915b34a7a89375
BLAKE2b-256 98454a64ea8f89e44ce0996f2d1f5d5f065b8a921ca61d520748350976086703

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_htmx_nav-0.3.0-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

0.3.1

2 files

This release

0.3.0 This release

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