Django + htmx

Get reactivity to your Django app without a JS framework or JS itself


14 min read

Quick intro

Django is an extremely powerful web framework that comes with nuts and bolts so that you can just focus on what you need to. If you are from a python background, I'm sure you would have used one of Flask or Django or FastAPI.

I've used it quite a bit to build many solutions, and I was always stunned by the neatness of class-based views, the minimal code that one had to write, the amount of documentation it has, and packages in PyPi that work for any situation one might have.

For the frontend, I use simple HTML and CSS making use of Django template framework, no fancy heavy-duty JS frameworks. Not that they are bad, just that I use a framework depending on the need, and not because the whole world is raving about it.

Using a JS framework or not is purely subjective. Trust me, I love JS frameworks ๐Ÿ˜, especially Vue

Practically speaking, every new framework/technology has a good amount of learning curve associated. If I cannot justify the need 100%, I'd prefer to stay away from it. Instead, I try and find simpler alternatives, weigh the pros and cons and make a final decision.

A bit of background

One such need for me was to have reactivity on a few screens in a web app developed using Django. When I started to dig deeper and find ways to handle it, one option that came up was to shift to JS frameworks for the front end. But it was too overkill in my case. Another option was to use javascript. I skipped that option as it may lead to a messy code over some time.

I then dug further and came across htmx. Per the htmx website,

htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext

When I read more about it, I decided to go ahead with it as it is simple, easy to use and does a neat job. In short, it does not load the entire page as in a typical Django way but simply loads the target DOM which is what I wanted.

Common scenarios to use htmx

There are quite a few scenarios where we do not have to load the entire page, and where we do not have to wait for the response only after we submit the page. Let's look at a few examples:

  • Inline validations are quite common in any web app. It makes a lot of sense to know the error message then and there rather than submit it finally

  • Quick edits are also a very intuitive and easy method to modify information.

  • Inline deletes and bulk deletes are also becoming common, especially within a table-like structure

  • Dynamic search is also considered a de facto these days

  • "Load More" button or "Infinite Scroll" is also more usable than clicking on page numbers using regular pagination

These are a few common scenarios that I think will be applicable in most of today's web apps. To look at the complete list of use cases, please take a look at this link.

Now that we've understood the need to use htmx, let's wet our feet. What's the fun without building something, is it not?

Time for some action

Even before you move ahead, I assume you are familiar with Django, which is why I do not elaborate on every single step

Let us build a super simple version that's intended to be your version of "Good Reads", which is why I called it 'My Book Shelf". To start with, let's keep it minimal. Below are the functionalities that we will add:

  • Sign up

  • Sign in/ Sign out

  • Add a book to your shelf

  • List books on your shelf

  • Quick edit using htmx

  • Delete using htmx

  • Dynamic search using htmx

Step 1: Bring up a Django project

I recently wrote a post about Django, I'd recommend referring to my previous post to bring up a Django project. Make sure to name the project as mybookshelf.

To ensure everything works fine, run the development server to see the default Django page.

Step 2: Create & Setup a Book app

Let's create a Django app and call it book. All logic related to our bookshelf will reside in this app.

python startapp book

Add and under book as they do not get added by default.

Create a class Book in

from django.contrib.auth.models import User
from django.db import models

# Create your models here.
class Book(models.Model):
    options = (
        ("just checking out", "Just Checking Out"),
        ("read", "Read"),
        ("reading", "Reading"),
        ("want to read", "Want to Read"),

    title = models.CharField(max_length=255)
    author = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    description = models.TextField()
    cover = models.ImageField(
        upload_to="covers/", blank=True, verbose_name="Cover Image"
    status = models.CharField(
        max_length=30, choices=options, default="want to read"
    created_by = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="books_createdby"
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    updated_by = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="books_updatedby"
    updated_at = models.DateTimeField(auto_now=True, editable=False)

    class Meta:
        ordering = ("-created_at",)

    def __str__(self):
        return self.title

Fields in the Book class are self-explanatory.

Do not run makemigrations at this stage as we will add a CustomUser class. We may get compilation errors for referencing User models. Just ignore them for the moment as we shall fix them in a minute.

Let's now add the methods required in and the corresponding path in

from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.views.decorators.http import require_http_methods
from django.views.generic import CreateView, ListView

from .forms import CustomUserCreationForm
from .models import Book

# Create your views here.
""" Landing page for MyBookShelf """
class HomeView(ListView):
    model = Book
    template_name = "book/index.html"

""" Method invoked for Sign up """
def register(request):
    if request.method == "POST":
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            return redirect("book:login")
            form = CustomUserCreationForm()
    return render(
        request, "registration/register.html", {"form": CustomUserCreationForm}

""" Method invoked to list books """
class BookListView(LoginRequiredMixin, ListView):
    model = Book
    template_name = "book/book-list.html"
    context_object_name = "books"
    paginate_by = 10

    def get_queryset(self):
        return Book.objects.filter(created_by=self.request.user)

""" Method invoked to add a book """
class BookCreateView(LoginRequiredMixin, CreateView):
    model = Book
    template_name = "book/book_create.html"
    fields = ["title", "author", "description", "cover", "status"]

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        form.instance.updated_by = self.request.user
        return super().form_valid(form)

    def get_success_url(self) -> str:
        return reverse_lazy("book:book-list")
from django.urls import path

from . import views

app_name = "book"

urlpatterns = [
    path("", views.HomeView.as_view(), name="home"),
    path("login/", auth_views.LoginView.as_view(), name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
    path("register/", views.register, name="register"),
    path("book-list", views.BookListView.as_view(), name="book-list"),
    path("book-create", views.BookCreateView.as_view(), name="book-create"),

Below is the code for At the moment I use Django's default username for authentication. Adding CustomUser at this stage so that in the future it can be easily modified to email, or more columns.

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class CustomUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = (

Don't forget to register the model in else it won't be displayed on your Django admin site. Keeping it simple at the moment ๐Ÿ˜Ž

from django.contrib import admin

from .models import Book

# Register your models here.

Step 3a: Add required template files

I generally maintain templates under the main project. I then create folders, one to hold the common htmls, and one for each app. This way, all templates reside in one place and it's easier this way.

Let's create 3 folders, base, book and registration under the folder templates.

Usually I keep the base.html and navbar.html under the base folder. base.html is more like a template which will be extended by all other htmls. This is done more as a DRY (Do not repeat yourself) principle. navbar.html holds the html code required for the navigation bar. Here's the code that is present in base.html

{% load static %}

<!doctype html>
<html lang="en">

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %} {% endblock %}</title>
    <link href="{% static 'css/main.css' %}" rel="stylesheet">

    {% block content %}
    {% endblock %}


And here's the code for navbar.html

<nav class="navbar navbar-expand-lg bg-body-tertiary mx-auto">
    <div class="container-fluid">
      <a class="navbar-brand logo fs-3" href="#">MyBookShelf</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      <div class="collapse navbar-collapse" id="navbarNavDropdown">
        <ul class="navbar-nav">
        {% if request.user.is_authenticated %}
            <li class="nav-item">
                <a class="nav-link active" aria-current="page" href="{% url 'book:book-list' %}">Home</a>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'book:book-create' %}">Add a Book</a>
            <li class="nav-item">
                <a class="btn button-primary" href="{% url 'book:logout' %}">Sign Out</a>
            <li class="nav-item ml-auto">
                <a class="nav-link">Welcome {{ request.user }} !</a>
        {% else %}
            <li class="nav-item">
                <a class="nav-link active" aria-current="page" href="#">Home</a>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'book:login' %}">Sign In</a>
            <li class="nav-item">
                <a class="btn button-primary" href="{% url 'book:register' %}">Sign Up</a>
        {% endif %}

Let's now create authentication related html files under registration folder. We need a login.html, a logout.html and a register.html. Just to keep things simple, I did not add any content to logout.html as on logout I just map it to the homepage (book\index.html).

Here's the code for login.html

{% extends "../base/base.html" %}

{% block title %} Sign in {% endblock %}

{% block content %}
    {% include "../base/navbar.html" %}

    {% if form.errors %}
        <p>Your username and password does not match ๐Ÿคจ</p>
    {% else %}
        <h3 style="text-align: center;color: #6a00f4">Sign In</h3>
    {% endif %}

    <div class="container text-center">
        <div class="row align-items-start">
            <div class="col"></div>
            <div class="col">
                <div class="login-form">
                    <form action="{% url 'book:login' %}" method="post">
                        {% csrf_token %}
                        {{ form.as_p }}
                        <button type="submit" class="btn button-primary">Sign in</button>
            <div class="col"></div>
{% endblock %}

Here's the code related to Sign up, it should be added to registration\register.html.

{% extends "../base/base.html" %}

{% block title %} Sign in {% endblock %}

{% block content %}
    {% include "../base/navbar.html" %}

    {% if form.errors %}
        <p>There is an error. Please correct and re-submit</p>
        <ul class="errorlist">
            {% for field, errors in form.errors.items %}
                {% for error in errors %}
                    <li>{{ error }}</li>
                {% endfor %}
            {% endfor %}
    {% else %}
        <h3 style="text-align: center;color: #6a00f4">Sign Up</h3>
    {% endif %}

    <div class="container text-center">
        <div class="row align-items-start">
            <div class="col"></div>
            <div class="col">
                <div class="login-form">
                    <form action="{% url 'book:register' %}" method="post">
                        {% csrf_token %}
                        {{ form.as_p }}
                        <button type="submit" class="btn button-primary">Sign up</button>
            <div class="col"></div>
{% endblock %}

Now for the default home page code, which is under book\index.html

{% extends "../base/base.html" %}
{% load static %}

{% block title %} My Book Shelf {% endblock %}

{% block content %}
    {% include "../base/navbar.html" %}
    <div class="container text-center" >
        <div class="row align-items-start">
            <div class="col col-margin">
                <h3>Want to maintain your own version of Good Reads?</h1>
                <p class="lead">Add books, categorize them as </p>
                <p style="color: #f20089"> Want to Read / Reading / Read /Just checking</p>
                <p class="lead">Sign up and keep track of your reading habits in your own book shelf !</p>
                <a class="btn button-primary" href="{% url 'book:register' %}">Sign Up</a>
            <div class="col col-margin">
                <img src="{% static 'images/bookshelf.png' %}" alt="bookshelf" class="img-fluid">
{% endblock %}

Once the htmls are developed, we then move on to create static files. I usually create static folder in the main project. Create 3 folders, one for css, one for images and one for js.

This is how my folder structure looks like with all the folders and files.

Note that the image below has a few additional files too which I did not discuss until now. Not to worry, they will be covered in the following sections, or you can find them in my github repo.

We also need to make sure that Django is able to navigate to the templates folder. In order to do that, add the following against DIRS property under TEMPLATES in your

It should now look like ๐Ÿ‘‡

        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [os.path.join(BASE_DIR, "templates")],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [

Step 4: Make a few additions to the main project

Now that we are done with our Book app, let's make sure that our main project knows about this app. We also need to make sure to add properties related to static files and media files.

  • Under INSTALLED_APPS in, add book

  •         STATIC_URL = "static/"
            STATICFILES_DIRS = [BASE_DIR / "static"]
            MEDIA_URL = "/media/"
            MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
      LOGIN_REDIRECT_URL = reverse_lazy("book:book-list")
      LOGIN_URL = "/login"
      LOGOUT_URL = "/logout"

    Make sure to update the like so to reference the newly created app as well as the path to static and media files ๐Ÿ‘‡

  •         from django.conf import settings as base
            from django.conf.urls.static import static
            from django.contrib import admin
            from django.urls import include, path
            urlpatterns = [
                path("", include("book.urls", namespace="book")),
            if base.DEBUG:
                urlpatterns += static(base.MEDIA_URL, document_root=base.MEDIA_ROOT)

We've now created the base project along with an app with Add and List functionality. Here is how the landing page looks like:

I signed up, and added a couple of books, and it looks like ๐Ÿ‘‡

Let's now get to the most important part of this post, the htmx part. To get a good grasp of htmx I highly recommend going through their documentation.

Step 5: Let's set up htmx in our bookshelf ๐Ÿš€

Install django-htmx in your virtual environment

pip install django-htmx

Add it to your installed apps


Add HTMXMiddleware to the MIDDLEWAREsection


Make sure to add the following to your base.html.

The first line is to include the actual js file. The second line is to handle csrf_token as Django requests will not function without it.

    <script src="{% static 'js/htmx.min.js' %}" defer></script>
      document.body.addEventListener('htmx:configRequest', (event) => {
        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';

Step 6: Adding htmx code to our book app

A few important properties of htmx that I used are:

  • hx-post - Invokes a post request

  • hx-get - Invokes a get request

  • hx-delete - Invokes a delete request

  • hx-target - Which part of the DOM should be replaced with the response?

  • hx-swap - HTML to be replaced in the DOM

  • hx-trigger - What triggers the event?

To get a mental mapping to use htmx, I follow the steps below:

  • Identify the element in a page that needs reactivity

  • Separate the output section corresponding to that element

  • Move that to a separate html file. Usually, the norm is to create a folder called partials under the usual templates folder.

  • Include the newly created html file in the parent file so that the contents will be replaced

  • In the parent html file, add htmx-related properties including the type of method to be invoked, the target element, what to swap, when to trigger and so on.

In our bookshelf, let's add the functionality of a quick edit of a few details of the book and deletion of the book without reloading the entire page.

Make the following changes to the existing book_list.html

Notice how we invoke the code to populate the table in separate book.html

{% extends "../base/base.html" %}

{% block content %}
    {% include "../base/navbar.html" %}
    <h3 style="text-align: center;">A peek into your book shelf!</h3>
    <section id="book-list">
        <div class="container">
            <div class="row border-bottom pb-2 mb-3 custom-header-bg text-white">
                <div class="col-2">Title</div>
                <div class="col-2">Author</div>
                <div class="col-2">Status</div>
                <div class="col-3">Modify?</div>
                <div class="col-3">Remove?</div>
        {% for book in books %}
            {% include "../book/partials/book.html" %}
        {% empty %}
            <p style="text-align: center;">No books in your shelf ๐Ÿ™ˆ</p>
        {% endfor %}
{% endblock %}
  • Let's now create a file book.html under the templates\partials folder. Add the following content

  •         <div class="container">
                    <div class="row mb-3">
                        <div class="col-2">{{ book.title }}</div>
                        <div class="col-2">{{ }}</div>
                        <div class="col-2">{{ book.status }}</div>
                        <div class="col-3">
                                hx-get="{% url 'book:quick-edit' %}"
                                hx-target="closest article"
                                    <button class="btn button-secondary">Quick edit</button>
                        <div class="col-3">
                            <button class="btn button-secondary" 
                            hx-confirm="Are you sure you want to delete this book?"
                            hx-target="closest article"
                            hx-swap="outerHTML swap:1s"
                            hx-delete="books/{{ }}/delete">

    In the case of the Quick edit button, we invoke a htmx-get to load the details of the book. The method also takes in a post method (in when the form is submitted. The hx-target maps to closest article in this case which refers to the article tag. It need not always be a tag. It can be an identifier too.

  • In the case of the Delete button, it is quite similar to the above except that we invoke hx-delete method. We also open up a small modal window that asks for a confirmation.

  • ๐Ÿ’ก
    Make sure to invoke the correct method from

That is all, quite simple! Initially, it takes a bit to wrap your head around htmx ๐Ÿ˜…. But that's the same with anything new, is it not? Once we understand the knack of things, it's just a matter of splitting, writing a few properties and invoking the child html.

Since the post is quite long already, I'm not adding the code snippet here related to Dynamic search. It's quite similar to the above. I will add a reference to my Github repo in the end, just in case you are interested to see the entire code ๐Ÿ˜€

Closing thoughts

htmx is a powerful library that can be used with many backend frameworks. I use it with Django, I'm sure it can be used with Flask too. I highly recommend using it when you have a case of loading a part of the page, firing validation as and when a user types something, and other reactive behavior.

As a matter of habit, I choose a framework/library only when the need is justified 200%, otherwise to me it's a better option to stick to a lightweight alternative as long as it satisfies my requirement.

Here is a link to my Github repo.


Simple working example of Django + htmx : Part 1

Simple working example of Django + htmx : Part 2

Article on dynamic search using htmx