Skip to content
cccskills
FreePublished on GitHub

django-htmx

Use when building dynamic Django web apps with htmx - partial rendering, HTMX responses, querystring tag, CSP

The English summary is being prepared. Showing the original description.

Works withPlaywright
See how to install

Files included (3)

  • SKILL.md14.4 KB
  • references/csp-nonce.md2.4 KB
  • references/testing-patterns.md6.1 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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

pip install django-htmx

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "django_htmx",
]

Add the middleware (order matters - after SessionMiddleware, before AuthenticationMiddleware):

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:

{% 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:

{% htmx_script minified=False %}

Jinja2 Configuration

If using Jinja2 templates, configure the global:

# 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:

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 TypeWhen to UseWhat Breaks If Wrong
HttpResponse (normal)Full page reload needed, or swapping entire documentUsing htmx-specific responses here causes no-op or unexpected behavior
HttpResponseClientRedirectNavigate to different URL without full reloadUsing plain HttpResponseRedirect leaves stale DOM; user sees old content
HttpResponseClientRefreshForce full page reload (stale session, permission change)Using redirect instead loses form data; using HttpResponse doesn't refresh
HttpResponseLocation"Boosted" navigation to new URLUsing redirect causes full reload; using HttpResponse shows wrong URL
HttpResponseStopPollingEnd polling loop (event finished, error unrecoverable)Omitting this keeps polling, wasting resources
Plain HttpResponse + HX-TriggerUpdate fragment and trigger client eventWithout trigger, client doesn't know to update related UI (e.g., badge count)
Plain HttpResponse + HX-Redirect headerClient-side redirect from server logicUsing HttpResponseClientRedirect directly is cleaner; manual header is for custom logic

HttpResponseClientRedirect

Use for navigation that should update browser history without full reload:

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:

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):

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:

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():

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:

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:

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:

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:

{% 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:

{{ 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:

pip install django-template-partials
{% 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 %}
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):

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:

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.

<!-- 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.

# 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.

# 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:

<!-- 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.

<div hx-get="/status" hx-trigger="every 1s">...</div>

Fix: Use longer intervals, or WebSockets for real-time:

<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.

<!-- 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:

<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.

# BAD: causes full page reload
return HttpResponseRedirect("/done/")

Fix: Use HttpResponseClientRedirect for htmx-aware redirects.

Testing

What You Can Test Server-Side

Fragment content:

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:

def test_redirect_header(self):
    response = self.client.post("/sensitive-action/")
    self.assertEqual(response["HX-Redirect"], "/activate-sudo/")

Out-of-band swaps:

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:

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:

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

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when doing structural code search and rewriting - ast-grep linting, refactoring, multi-language patterns

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when building GBA games with the BPCore Lua engine - entity, sprite and tilemap functions, SRAM save and load, link cable multiplayer protocol, camera and scrolling, or optimization patterns

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

celery

Free

Use when running background tasks with Celery - worker and broker configuration (Redis, RabbitMQ), task routing by name vs queue, chains/groups/chords, retry patterns (autoretry_for, retry_backoff), acks_late semantics, failure detection, and monitoring with Flower

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

django

Free

Use when building Django applications - security hardening, authentication and permissions, ORM optimization, PostgreSQL features, Django 6.0, migrations, testing, and ecosystem libraries

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when customizing Django Admin - save_formset, get_search_results, formsets, queryset optimization, db_index, custom URLs

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

See all skills by CodeAtCode

Report a problem with this skill