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 firesHX-Triggerevents on the client.HtmxRedirectResponse– a response that sends anHX-Redirectheader to navigate the browser.HtmxMessagesMiddleware– automatically forwards Django messages to the client via theHX-Triggerheader on every HTMX request.HtmxFormResponseMixin– aFormView/CreateViewmixin that replaces the success redirect with an HTMX trigger response.HtmxListView– aListViewsubclass with built-in URL-based filtering, column sorting, and elided pagination.- Filter helpers – parse
field.filter_type=valuequery parameters safely into Django ORM filter dicts. - Cotton components – drop-in table, header cell, and pager components styled for Bootstrap 5.
- Template filters –
get_attrandget_key_valuefor accessing object attributes and dict values in templates. - Built-in JavaScript helper – a
django-htmx-plus.jsES 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 useHtmxResponse) 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
ModalandOffcanvasoffwindow.bootstrap, so Bootstrap 5's regular JS bundle must be loaded (as a plain<script>, before{% htmx_plus_script %}) so it can attach itself towindow.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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.