Skip to main content

django-htmx-plus

A Django utility library that works with django-cotton to provide ready-made views, mixins, middleware, response helpers, and components for building HTMX-powered list views with filtering, sorting, and pagination.

This package was created for my own projects but it was handy enough to share, I learned how to use HTMX with Django and Boostrap Modals from the following two posts from Josh Karamuth:


Features

  • HtmxResponse – a 204 No Content response that fires HX-Trigger events on the client.
  • HtmxRedirectResponse – a response that sends an HX-Redirect header to navigate the browser.
  • HtmxMessagesMiddleware – automatically forwards Django messages to the client via the HX-Trigger header on every HTMX request.
  • HtmxFormResponseMixin – a FormView/CreateView mixin that replaces the success redirect with an HTMX trigger response.
  • HtmxListView – a ListView subclass with built-in URL-based filtering, column sorting, and elided pagination.
  • Filter helpers – parse field.filter_type=value query parameters safely into Django ORM filter dicts.
  • Cotton components – drop-in table, header cell, and pager components styled for Bootstrap 5.
  • Template filters – get_attr and get_key_value for accessing object attributes and dict values in templates.
  • Built-in JavaScript helper – a django-htmx-plus.js ES module that wires Bootstrap 5 Modals and Offcanvases to HTMX swap events, with a {% htmx_plus_script %} template tag to include it.

Requirements

  • Python 3.11+
  • Django (any version compatible with the above)
  • django-cotton ≥ 2.6

Installation

pip install django-htmx-plus

Add to INSTALLED_APPS and configure middleware in settings.py:

INSTALLED_APPS = [
    # ...
    "django_cotton",
    "django_htmx_plus",
]

MIDDLEWARE = [
    # ...
    "django_htmx_plus.middleware.HtmxMessagesMiddleware",
]

Usage

HtmxResponse

Return a 204 No Content response that triggers one or more HTMX events on the client:

from django_htmx_plus.http import HtmxResponse

def my_view(request):
    # ... do some work ...
    return HtmxResponse(triggers=["itemUpdated", "refreshStats"])

The response sets HX-Trigger: itemUpdated,refreshStats, which HTMX picks up to re-fetch any elements listening for those events.


HtmxRedirectResponse

Instruct HTMX to navigate the browser to a new URL without a traditional HTTP redirect:

from django_htmx_plus.http import HtmxRedirectResponse

def my_view(request):
    return HtmxRedirectResponse(destination="/dashboard/")

HtmxMessagesMiddleware

Once the middleware is installed, any Django message added during an HTMX request is automatically serialised into the HX-Trigger header as a messages key:

{
  "messages": [
    {"message": "Record saved.", "tags": "success"}
  ]
}

If the view already sets its own HX-Trigger header (either string or JSON object syntax), the middleware merges the messages in without overwriting existing triggers.

Listen for the messages event in your HTMX setup to display the messages, for example:

document.body.addEventListener("messages", (event) => {
    event.detail.value.forEach(({message, tags}) => {
        showToast(message, tags); // your toast implementation
    });
});

HtmxFormResponseMixin

Mix into any FormView or CreateView to replace the default success redirect with an HTMX trigger response:

from django.views.generic.edit import CreateView
from django_htmx_plus.mixins import HtmxFormResponseMixin
from myapp.models import Article
from myapp.forms import ArticleForm

class ArticleCreateView(HtmxFormResponseMixin, CreateView):
    model = Article
    form_class = ArticleForm
    template_name = "articles/form.html"

    valid_triggers = ["articleCreated"]
    success_message = "Article created successfully."

After a valid submission, form.save() is called, an optional Django success message is queued, and an HtmxResponse carrying articleCreated in HX-Trigger is returned.

Attribute Type Description
valid_triggers List[str] Events to include in HX-Trigger on success.
success_message str Optional Django success message to queue.

HtmxListView

A drop-in replacement for Django's ListView that adds URL-driven filtering, sorting, and elided pagination:

from django.views.generic import ListView
from django_htmx_plus.views import HtmxListView
from myapp.models import Article

class ArticleListView(HtmxListView):
    model = Article
    template_name = "articles/list.html"
    paginate_by = 10
    hx_target_id = "#article-table"

    # Restrict filtering and sorting to these fields only
    fields = ("id", "title", "status", "created_at")

    # Optional custom column labels
    labels = {
        "created_at": "Date Created",
    }

URL-based filtering

Filters are expressed as field_name.filter_type=value query parameters:

Filter key Django lookup Example
eq exact status.eq=published
ieq iexact title.ieq=hello
gt / gte / lt / lte gt / gte / lt / lte views.gte=100
like / ilike like / ilike title.ilike=django
sw / isw startswith / istartswith title.sw=Django
ew / iew endswith / iendswith title.ew=plus
in in status.in=['draft','published']
nl isnull deleted_at.nl=True
rng range created_at.rng=['2024-01-01','2024-12-31']
sch search body.sch=htmx

Only fields listed in fields are accepted — there is no wildcard/bypass value.

"pk" is always added to fields automatically if it isn't already present, so rows remain uniquely identifiable even if you don't list it explicitly. The <c-tables.htmx_table /> component hides the pk column by default — pass show_pk=True in the context to render it. You can force "pk" to show in the list at the desired position by adding it to the fields list manually.

Sorting

Add order_by=field_name (or order_by=-field_name for descending) to the query string. Only fields in fields are permitted.

Changing the page size

Add paginate_by=N to the query string to override the view's default page size for that request — this is also what the <c-tables.htmx_table /> "Show N entries" selector submits when paginate_by is set on the view. The value is applied via a plain int() conversion with no clamping, so views exposed to untrusted users should validate/clamp it themselves (e.g. in an overridden get_context_data/setup).

Context variables

Variable Description
query_params URL-encoded query string (without page).
page_range Elided page range for pagination controls.
order_by The currently active ordering field.
path The current request path.
hx_target_id The HTMX target element ID.
query Full query string including order_by.
filter_query Query string containing only filter parameters.
filters Template-ready dict mapping plain field names to their filter values.
fields Dict with keys (field names, always including pk) and labels (display names).

Transforming rows

Override get_transform_data to post-process each page's rows before they reach the template, without touching get_queryset or get_context_data:

class ArticleListView(HtmxListView):
    model = Article
    fields = ("title", "status", "created_at")

    def get_transform_data(self, objects):
        rows = super().get_transform_data(objects)
        for row in rows:
            row["status"] = row["status"].title()
        return rows

Cotton Table Components

django-htmx-plus ships a set of django-cotton components for rendering sortable, paginated HTMX tables with Bootstrap 5.

<c-tables.htmx_table />

Renders a full table with auto-generated sortable headers, rows, and an optional pager:

{% load cotton_extras %}

<c-tables.htmx_table class="table table-striped" />

The component uses the fields, objects, order_by, path, filter_query, hx_target_id, page_obj, and paginator context variables provided automatically by HtmxListView.

When paginator is present (i.e. paginate_by is set on the view), the component also renders a "Show N entries" <select> (10/25/50/100) above the table that submits a paginate_by query parameter via hx-get.

<c-tables.header_cell name="field_name" />

Renders a single <th> with an hx-get attribute that toggles ascending/descending order and a chevron icon indicating the current sort direction:

<c-tables.head>
    <c-tables.header_cell name="title">Title</c-tables.header_cell>
    <c-tables.header_cell name="created_at">Date</c-tables.header_cell>
</c-tables.head>

<c-tables.pager />

Renders a Bootstrap 5 pagination control with previous/next buttons and elided page numbers, all wired to hx-get.

Icon components

Component Description
<c-icons.chevron_up /> Up chevron (ascending sort indicator).
<c-icons.chevron_down /> Down chevron (descending sort indicator).
<c-icons.chevron_left /> Left chevron (previous page).
<c-icons.chevron_right /> Right chevron (next page).

All icon components accept an optional add_class attribute to append extra CSS classes.


Template filters

Load the cotton_extras tag library in any template:

{% load cotton_extras %}
Filter Description Example
get_attr Get an attribute from an object by name. {{ item|get_attr:"title" }}
get_key_value Get a value from a dict by key. {{ my_dict|get_key_value:"name" }}

Built-in JavaScript Helper

django-htmx-plus ships a small ES module (django-htmx-plus.js) that integrates Bootstrap 5 Modals and Offcanvases with HTMX swap events, so they open and close automatically based on HTMX responses.

How it works

Behaviour Trigger
Show a Modal or Offcanvas HTMX swaps content into the element → htmx:afterSwap fires and calls .show().
Hide a Modal or Offcanvas HTMX receives an empty response targeting the element → htmx:beforeSwap fires, calls .hide(), and cancels the swap.
Reset Modal body Bootstrap's hidden.bs.modal event fires → the modal body is cleared to "".

Setup

Mark your Bootstrap Modal root elements with data-htmx-plus-modal="<id>" and your Offcanvas root elements with data-htmx-plus-offcanvas="<id>", where id is the id of the element that will be swapped,:

<!-- Bootstrap Modal managed by django-htmx-plus -->
<div class="modal fade" data-htmx-plus-modal="dialog">
    <div id="dialog" class="modal-dialog">
        <!-- Modal content will be swapped here by HTMX and shown/hidden by django-htmx-plus.js -->
    </div>
</div>

<!-- Bootstrap Offcanvas managed by django-htmx-plus -->
<div id="flyout" class="offcanvas offcanvas-end" data-htmx-plus-offcanvas="flyout">
    <!-- Offcanvas content will be swapped here by HTMX and shown/hidden by django-htmx-plus.js -->
</div>

Then point an HTMX element at the matching target ID:

<button hx-get="/person/add/" hx-target="#dialog">
    Add Person
</button>
  • When the response has content the modal/offcanvas is shown automatically.
  • When the server returns an empty 200 (or you use HtmxResponse) the modal/offcanvas is hidden automatically.

Including the script

Use the {% htmx_plus_script %} template tag to render the <script> tag. The script is loaded as an ES module and optionally forwards a CSP nonce if one is present in the template context:

{% load django_htmx_plus %}

<!-- Place near the bottom of your base template, after Bootstrap JS -->
{% htmx_plus_script %}

This renders:

<script src="/static/django_htmx_plus/django-htmx-plus.js" type="module"></script>

If a nonce variable is present in the template context it is automatically added as a nonce="..." attribute.

Note: The script reads Modal and Offcanvas off window.bootstrap, so Bootstrap 5's regular JS bundle must be loaded (as a plain <script>, before {% htmx_plus_script %}) so it can attach itself to window.bootstrap. No import map or bundler is required.

For example

<script src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
{% htmx_plus_script %}

License

MIT — see LICENSE for details.

Metadata

Release files for django-htmx-plus 4.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-htmx-plus 4.0.3
File Size Uploaded Provenance Actions
django_htmx_plus-4.0.3.tar.gz 20.6 kB With provenance Details for django_htmx_plus-4.0.3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-htmx-plus 4.0.3
File Interpreter ABI Platform Actions
django_htmx_plus-4.0.3-py3-none-any.whl Python 3 none any Details for django_htmx_plus-4.0.3-py3-none-any.whl

Total release size: 43.7 kB

Release files / django_htmx_plus-4.0.3.tar.gz

Download URL django_htmx_plus-4.0.3.tar.gz
Size 20.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d53d4f8dfbc8b42753262214130ded1863ee25d699dce5e2c4c8de745d4a03d7
BLAKE2b-256 checksum
How to use checksums
1c869529135cccda5d18c4bde478dfe2d296a9d2978b5fdb34d6f063eb4ce412
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Release files / django_htmx_plus-4.0.3-py3-none-any.whl

Download URL django_htmx_plus-4.0.3-py3-none-any.whl
Size 23.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
538513631da5aad7acb87ffa10cb1d560bf93a4c8a42bf5ebe198e76b8bfd1c8
BLAKE2b-256 checksum
How to use checksums
44bf2af89567d3027de7be15bbd4be4499887818e9bb001c79e308e661823af9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Security for django-htmx-plus 4.0.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.

Select from 2 files

Attestation for django_htmx_plus-4.0.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

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.

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