Python

Building Interactive UIs with Django and HTMX

Building Interactive UIs with Django and HTMX

HTMX lets you build modern, interactive web applications without writing JavaScript. Instead of building a separate frontend that communicates via JSON APIs, you return HTML fragments from your server and let HTMX swap them into the page. Let's see how to integrate it with Django.

Why HTMX?

Traditional SPAs require complex JavaScript frameworks, build pipelines, and state management libraries. HTMX takes a radically different approach: it extends HTML with attributes that enable dynamic interactions.

  • No build step required - just include the script tag
  • Server-side rendering means better SEO and faster initial loads
  • Your Django templates become your 'components'
  • Reduced complexity - no separate frontend codebase
  • Progressive enhancement - works without JavaScript (mostly)
  • Smaller bundle size - HTMX is ~14KB gzipped
💡 HTMX makes HTML more powerful without adding complexity. It's not about replacing JavaScript - it's about not needing it for common patterns.

Installation

First, add HTMX and django-htmx to your project:

python
# Install the Django integration
pip install django-htmx

# settings.py
INSTALLED_APPS = [
    # ...
    'django_htmx',
]

MIDDLEWARE = [
    # ...
    'django_htmx.middleware.HtmxMiddleware',
]

Then include HTMX in your base template:

html
<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>

Example 1: Like Button

Let's build a like button that updates without a page refresh. This is a classic example that shows the power of HTMX.

html
<!-- templates/partials/like_button.html -->
<button hx-post="{% url 'like_post' post.id %}"
        hx-swap="outerHTML"
        class="flex items-center gap-2 px-4 py-2 rounded-lg
               {% if user_liked %}bg-red-100 text-red-600{% else %}bg-gray-100{% endif %}">
    <span>❤️</span>
    <span>{{ post.like_count }}</span>
</button>
python
# views.py
from django.views.decorators.http import require_POST
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse

@require_POST
def like_post(request, post_id):
    post = get_object_or_404(Post, id=post_id)

    # Toggle like
    like, created = PostLike.objects.get_or_create(
        post=post, user=request.user
    )
    if not created:
        like.delete()
        user_liked = False
    else:
        user_liked = True

    # Update count
    post.like_count = post.likes.count()
    post.save()

    return TemplateResponse(
        request,
        'partials/like_button.html',
        {'post': post, 'user_liked': user_liked}
    )
How does this work?
1. User clicks the button
2. HTMX intercepts the click and sends a POST request to /posts/123/like/
3. The view toggles the like and returns the updated button HTML
4. HTMX replaces the button (hx-swap="outerHTML") with the response
5. The UI updates instantly - no page reload needed

The key insight: the server returns HTML, not JSON. No client-side state to manage!

Example 2: Infinite Scroll

Loading more content as the user scrolls is another common pattern. HTMX makes this trivial:

html
<!-- templates/posts/list.html -->
<div id="post-list">
    {% for post in posts %}
        {% include 'partials/post_card.html' %}
    {% endfor %}

    {% if has_next %}
    <div hx-get="{% url 'post_list' %}?page={{ next_page }}"
         hx-trigger="revealed"
         hx-swap="outerHTML"
         hx-select="#post-list > *">
        <span class="loading">Loading more...</span>
    </div>
    {% endif %}
</div>

The hx-trigger="revealed" attribute tells HTMX to fire the request when the element scrolls into view. The hx-select extracts just the post cards from the response, avoiding nested containers.

Example 3: Live Search

html
<!-- Search input with debounce -->
<input type="search"
       name="q"
       hx-get="{% url 'search' %}"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#search-results"
       placeholder="Search posts...">

<div id="search-results">
    <!-- Results appear here -->
</div>

The delay:300ms modifier debounces the input, preventing a request on every keystroke. Results are loaded into #search-results as the user types.

Key Takeaways

  1. HTMX attributes (hx-get, hx-post, hx-swap, etc.) make HTML interactive
  2. Server returns HTML fragments, not JSON - use Django templates
  3. No JavaScript framework or build pipeline required
  4. Progressive enhancement friendly - degrades gracefully
  5. Perfect for Django's template-centric architecture
  6. Use django-htmx for request detection and response helpers
🎯 Pro tip: Use Django's TemplateResponse and template partials to keep your code DRY. Define reusable fragments that work for both full page loads and HTMX requests.

Related Posts

Welcome to Warbler

An introduction to Warbler, our modern blogging platform built for developers who love clean, fast, and beautiful experiences.

1 min read

Python Best Practices for 2024

Essential Python patterns and practices every developer should know. From type hints to dataclasses, level up your Python code.

1 min read

Comments

Log in to leave a comment.

Log In

Loading comments...