Use when building dynamic Django web apps with htmx - partial rendering, HTMX responses, querystring tag, CSP
Pro scans all 3 files and shows the line behind each finding
Scanned 10/2/2026
npx -y skills add CodeAtCode/oss-ai-skills --skill django-htmx --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Django Htmx?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/codeatcode-django-htmx)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: django-htmx
description: Use when building dynamic Django web apps with htmx - partial rendering, HTMX responses, querystring tag, CSP
metadata:
author: mte90
version: 1.0.1
tags:
- django
- htmx
- python
- web
- frontend
- partial-rendering
- ajax
---
# Django HTMX
Django-htmx provides seamless integration between Django and htmx for building modern, dynamic web applications without writing complex JavaScript.
**Versions**: django-htmx 1.16.0 + Django 6.0 fully compatible. Python 3.10 → 3.14 supported.
## Installation
```bash
pip install django-htmx
```
Add to `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
...
"django_htmx",
]
```
Add the middleware (order matters - after SessionMiddleware, before AuthenticationMiddleware):
```python
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_htmx.middleware.HtmxMiddleware", # Required for request.htmx
"django.contrib.messages.middleware.MessageMiddleware",
...
]
```
## Setup
### Base Template
Load the template tag and include the htmx script once in your base template:
```django
{% load django_htmx %}
<!DOCTYPE html>
<html>
<head>
{% htmx_script %}
</head>
<body hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>
{% block content %}{% endblock %}
</body>
</html>
```
The `hx-headers` attribute on `<body>` ensures all htmx requests carry the CSRF token. Without this, POST/PUT/DELETE requests fail with 403.
For debugging, use the unminified version:
```django
{% htmx_script minified=False %}
```
### Jinja2 Configuration
If using Jinja2 templates, configure the global:
```python
# settings.py or jinja2 config
from django_htmx.jinja import htmx_script
def environment(**options):
from jinja2 import Environment
env = Environment(**options)
env.globals.update({"htmx_script": htmx_script})
return env
```
Then in templates: `{{ htmx_script() }}`
**See**: `references/csp-nonce.md` for Content-Security-Policy integration.
## Core Concepts
### Request Detection
The middleware adds `request.htmx` to detect htmx requests:
```python
from django.shortcuts import render
def my_view(request):
if request.htmx:
template_name = "partial.html"
else:
template_name = "full.html"
return render(request, template_name)
```
### HtmxDetails Attributes
The `request.htmx` object provides:
- `request.htmx` - Boolean, True if request is from htmx
- `request.htmx.boosted` - True if from boosted element (hx-boost)
- `request.htmx.current_url` - Current URL from HX-Current-URL header
- `request.htmx.current_url_abs_path` - Absolute path form of current_url
- `request.htmx.history_restore_request` - True for history restoration
- `request.htmx.target` - Target element ID from HX-Target header
- `request.htmx.trigger` - Trigger element ID from HX-Trigger header
- `request.htmx.trigger_name` - Trigger element name from HX-Trigger-Name header
- `request.htmx.prompt` - User response to hx-prompt attribute
- `request.htmx.triggering_event` - Deserialized JSON from event-header extension
## HTTP Response Classes (Decision Guide)
Choose the right response type based on what should happen on the client:
| Response Type | When to Use | What Breaks If Wrong |
|---------------|-------------|---------------------|
| `HttpResponse` (normal) | Full page reload needed, or swapping entire document | Using htmx-specific responses here causes no-op or unexpected behavior |
| `HttpResponseClientRedirect` | Navigate to different URL without full reload | Using plain `HttpResponseRedirect` leaves stale DOM; user sees old content |
| `HttpResponseClientRefresh` | Force full page reload (stale session, permission change) | Using redirect instead loses form data; using `HttpResponse` doesn't refresh |
| `HttpResponseLocation` | "Boosted" navigation to new URL | Using redirect causes full reload; using `HttpResponse` shows wrong URL |
| `HttpResponseStopPolling` | End polling loop (event finished, error unrecoverable) | Omitting this keeps polling, wasting resources |
| Plain `HttpResponse` + `HX-Trigger` | Update fragment and trigger client event | Without trigger, client doesn't know to update related UI (e.g., badge count) |
| Plain `HttpResponse` + `HX-Redirect` header | Client-side redirect from server logic | Using `HttpResponseClientRedirect` directly is cleaner; manual header is for custom logic |
### HttpResponseClientRedirect
Use for navigation that should update browser history without full reload:
```python
from django_htmx.http import HttpResponseClientRedirect
def sensitive_view(request):
if not sudo_mode.active(request):
return HttpResponseClientRedirect("/activate-sudo/")
...
```
**What breaks**: A plain `HttpResponseRedirect` causes htmx to follow the redirect as a normal request, which may swap the wrong fragment or leave the original DOM intact.
### HttpResponseClientRefresh
Force a full page reload:
```python
from django_htmx.http import HttpResponseClientRefresh
def partial_table_view(request):
if page_outdated(request):
return HttpResponseClientRefresh()
...
```
**What breaks**: Without this, the client keeps showing stale data. Use when session expired, permissions changed, or cache invalidation occurred.
### HttpResponseLocation
Trigger client-side "boosted" navigation (hx-boost behavior):
```python
from django_htmx.http import HttpResponseLocation
def wait_for_completion(request, action_id):
...
if action.completed:
return HttpResponseLocation(f"/action/{action.id}/completed/")
...
```
**What breaks**: Using a redirect causes a full page reload, losing the htmx benefits.
### HttpResponseStopPolling
End a polling loop:
```python
from django_htmx.http import HttpResponseStopPolling
def my_pollable_view(request):
if event_finished():
return HttpResponseStopPolling()
return render(request, "status.html", {"status": "running"})
```
Or use the constant with `render()`:
```python
from django_htmx.http import HTMX_STOP_POLLING
def my_pollable_view(request):
if event_finished():
return render(request, "event-finished.html", status=HTMX_STOP_POLLING)
```
## Response Modifying Functions
These wrap an `HttpResponse` to add htmx-specific headers:
### push_url / replace_url
Update browser history without navigation:
```python
from django_htmx.http import push_url
def leaf_select(request, leaf_id):
...
response = render(request, "leaf-detail.html", {"leaf": leaf})
return push_url(response, f"/leaf/{leaf.id}")
```
Use `replace_url()` to replace current history entry instead of pushing.
### reswap / retarget / reselect
Override htmx behavior server-side:
```python
from django_htmx.http import reswap, retarget, reselect
def conditional_swap(request):
response = render(request, "row.html", {"row": row})
if row.is_special:
reswap(response, "afterbegin") # Override hx-swap
retarget(response, "#special-container") # Override hx-target
return reselect(response, ".data-row") # Override CSS selector
```
### trigger_client_event
Trigger custom JavaScript events after swap:
```python
from django_htmx.http import trigger_client_event
def end_of_process(request):
response = render(request, "done.html")
return trigger_client_event(
response,
"showConfetti",
{"colours": ["purple", "red", "pink"]},
after="swap", # "receive", "settle", or "swap"
)
```
The event name (`showConfetti`) must match a handler registered via `htmx.on()`.
## Template Tags
### querystring Filter (Django 4.1+)
Rebuild query strings for filter/pagination links:
```django
{% load django_htmx %}
<!-- Keep all filters, flip `outdoors` off -->
<a hx-get="{% querystring outdoors=None %}" hx-target="#list"
hx-push-url="true">hide outdoors</a>
<!-- Paginate -->
<a hx-get="{% querystring page=page.next %}" hx-target="#list">next</a>
```
### json_script
Embed Python data for client-side use:
```django
{{ row.config|json_script:"row-config" }}
<script>
const cfg = JSON.parse(document.getElementById('row-config').textContent);
htmx.trigger('#row', 'config-ready', cfg);
</script>
```
## Best Practices
### Partial Rendering with Template Partials
Use `django-template-partials` for efficient fragment rendering:
```bash
pip install django-template-partials
```
```django
{% extends "_base.html" %}
{% load partials %}
{% block main %}
{% partialdef country-table inline %}
<table id="country-data">
{% for country in countries %}
<tr><td>{{ country.name }}</td></tr>
{% endfor %}
</table>
{% endpartialdef %}
{% endblock main %}
```
```python
def country_listing(request):
template = "countries.html"
if request.htmx:
template += "#country-table" # Render partial only
return render(request, template, {"countries": Country.objects.all()})
```
### Caching
Keep the cached template loader enabled (it's default, but easy to break):
```python
TEMPLATES = [{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"OPTIONS": {
"loaders": [
("django.template.loaders.cached.Loader", [
"django.template.loaders.app_directories.Loader",
"django.template.loaders.filesystem.Loader",
]),
],
},
}]
```
Without it, Django recompiles templates on every request, dominating CPU for htmx partials.
**Diagnosis**: If `py-spy` shows `django.template.base.compile` at the top, the cache is off.
### Vary Headers
Tell caches that content differs by htmx request:
```python
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_headers
@cache_control(max_age=300)
@vary_on_headers("HX-Request")
def my_view(request):
...
```
## Anti-Patterns
### 1. Business Logic in Template Fragments
**Wrong**: A partial that needs data the parent page didn't load.
```django
<!-- BAD: template tries to access data not in context -->
{% partialdef user-stats %}
{{ user.profile.score }} {# user not passed to partial #}
{% endpartialdef %}
```
**Fix**: Ensure the view serving the fragment has all required context.
### 2. Returning JSON to hx-get
**Wrong**: htmx expects HTML by default.
```python
# BAD: htmx silently ignores JSON responses
def data_view(request):
return JsonResponse({"data": items})
```
**Symptom**: No error, but nothing updates in the DOM.
**Fix**: Return HTML fragments, or use `hx-trigger` with JavaScript to handle JSON.
### 3. Missing hx-swap-oob for Out-of-Band Updates
**Wrong**: Updating a badge without telling htmx where to put it.
```python
# BAD: client doesn't know to update badge
return render(request, "order-status.html", {"status": "shipped"})
```
**Fix**: Use `hx-swap-oob` to update multiple fragments:
```django
<!-- In the fragment response -->
<span id="badge" hx-swap-oob="true">{{ new_count }}</span>
```
### 4. Expensive Polling with every="1s"
**Wrong**: Self-inflicted load generator.
```django
<div hx-get="/status" hx-trigger="every 1s">...</div>
```
**Fix**: Use longer intervals, or WebSockets for real-time:
```django
<div hx-get="/status" hx-trigger="every 5s">...</div>
```
Or load the `ws` extension for push-based updates.
### 5. Missing CSRF Token
**Wrong**: POST requests fail with 403.
```django
<!-- BAD: no CSRF token -->
<body>
<form hx-post="/submit">...</form>
</body>
```
**Symptom**: 403 errors that look like template bugs.
**Fix**: Include token in `hx-headers` on `<body>` (see Setup section) or per-form:
```django
<form hx-post="/submit" hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>
```
### 6. Forgetting HX-Redirect for Client-Side Redirects
**Wrong**: Using Django's `HttpResponseRedirect` in htmx context.
```python
# BAD: causes full page reload
return HttpResponseRedirect("/done/")
```
**Fix**: Use `HttpResponseClientRedirect` for htmx-aware redirects.
## Testing
### What You Can Test Server-Side
**Fragment content**:
```python
from django.test import TestCase
class HtmxTests(TestCase):
def test_fragment_content(self):
response = self.client.get(
"/partial/",
HTTP_HX_REQUEST="true", # Sets HX-Request header
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Expected fragment content")
```
**HX-Response headers**:
```python
def test_redirect_header(self):
response = self.client.post("/sensitive-action/")
self.assertEqual(response["HX-Redirect"], "/activate-sudo/")
```
**Out-of-band swaps**:
```python
def test_oob_swap(self):
response = self.client.post("/update-badge/")
# Check the response contains hx-swap-oob attribute
self.assertIn(b'hx-swap-oob="true"', response.content)
```
**CSRF handling**:
```python
def test_csrf_required(self):
response = self.client.post("/action/", HTTP_HX_REQUEST="true")
self.assertEqual(response.status_code, 403) # No CSRF token
response = self.client.post(
"/action/",
HTTP_HX_REQUEST="true",
HTTP_X_CSRFTOKEN=self.client.cookies["csrftoken"].value,
)
self.assertEqual(response.status_code, 200)
```
### What You Cannot Test Server-Side
- **Actual DOM swaps**: Django tests don't render HTML in the browser
- **hx-trigger timing**: Polling intervals, debouncing
- **Client-side event handlers**: `htmx.on()` handlers triggered by `HX-Trigger`
- **hx-swap behavior**: How content is actually inserted (use Playwright/Cypress for this)
**For client-side behavior**: Use end-to-end tests with Playwright or Cypress.
## Type Checking
Extend `HttpRequest` for type hints:
```python
from django.http import HttpRequest as HttpRequestBase
from django_htmx.middleware import HtmxDetails
class HttpRequest(HttpRequestBase):
htmx: HtmxDetails
```
## Deep Dives
Load these reference files for detailed guidance:
- `references/csp-nonce.md` - Content-Security-Policy nonce integration (when strict CSP breaks htmx)
- `references/testing-patterns.md` - Extended testing strategies and e2e setup
## References
- **Official Documentation**: https://django-htmx.readthedocs.io/
- **GitHub Repository**: https://github.com/adamchainz/django-htmx
- **htmx Reference**: https://htmx.org/reference/
- **jvns.ca – More nice Django things**: https://jvns.ca/blog/2026/07/21/more-nice-django-things/
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!