django htmx article feature image

How to integrate htmx in Django with example

Htmx is short for high power tools for HTML. It simplifies tedious work for developers. A developer can access modern browser features without writing a single line of Javascript. Yes! that is how powerful htmx is even though it has a size of approximately 10k. According to it’s creator, Carson Gross:

“htmx allows you to access 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”


Now, integrating htmx in Django might get a little confusing if you are just starting out. Luckily, htmx comes with excellent documentation and examples. There are also amazing Github repositories that particularly stands out. One of those repositories ‘guettli / django-htmx-fun‘ has been created by Thomas Güttler. It showcases how we can leverage the power of htmx to build a single page application. The application is a virtual diary where the users can add notes. It supports endless scrolling or otherwise known as lazy loading. Furthermore, it is a perfect and easy example for beginners of htmx. Thus, the remainder of the tutorial will focus on explaining the working principles of the same.

This tutorial assumes that you are already familiar with Django. If you are yet to explore Django you could explore some of my older tutorials here.

The tutorial is divided into four small sections. Each section focuses on one tiny step at a time. This approach reduces the overall complexity of the entire tutorial and promotes ease of learning. The sections are mentioned below :

Section A: Installation

The installation process of the application is quite straighforward and documented well in the projects readme file. Open your terminal and fire up the following commands:


python3 -m venv django-htmx-fun-env
cd django-htmx-fun-env
. bin/activate
pip install -e git+

Using SSH

python3 -m venv django-htmx-fun-env
cd django-htmx-fun-env
. bin/activate
pip install -e git+ssh://

Note: To run the last command you need to have ssh setup in your machine. Follow the links provided below to setup SSH in your machine

Next, run database migrations migrate

Create a superuser createsuperuser

Run the webserver runserver

Now you can open up your browser and navigate to to view the diary and

Section B: Naming Pattern

Thomas Güttler follows a naming pattern that has been mentioned in his projects readme file. It is imperative that we too understand it meticulously and the same has been provided in Table 1.

Trailing StringFBVReturnsURLNotes
_page():foo_page(request, …)HttpResponse with a full-page/fooOnly HTTP-GET
_hx():foo_hx(request, …)HttpResponse with a HTML fragment/foo_hxCalled via HTTP-GET
_hxpost():foo_hxpost(request, …) HttpResponse with a HTML fragment /foo_hxpostCalled via HTTP-POST. Use require_POST decorator for additional protection
_json():foo_json(request, …)JSONResponse/foo_json
_html():Python methodHTML SafeStringUsually created via format_html
Table 1: Naming pattern

Let us now look at its model Note and how it has been defined.

Section C: Model and Add to Admin Panel

Create Diary Model

We want to store our inputs into a DB for which we would need to define a model. The model is called Note and its definition is located at diary/

from django.db import models
from django.utils import timezone
from django.utils.html import format_html

class Note(models.Model):
    datetime = models.DateTimeField(
    title = models.CharField(max_length=1024, default='')
    text = models.TextField(default='', blank=True)

    def __str__(self):
        return 'Note {} {}'.format(self.datetime.strftime('%Y-%m-%d'), self.title)

Add Model ‘Note’ to the Admin Panel

Once we add the model to our admin panel we can easily perform CRUD operations on it by navigating to The source code is located at diary/

from diary.models import Note
from django.contrib import admin

class NoteAdmin(admin.ModelAdmin):
    model = Note
    list_display = ['id', 'datetime', 'title', 'text']
    ordering = ['-datetime', '-id'], NoteAdmin)

I am not explaining much about the Model and the Admin section. These topics are covered in-depth in one of my favourite courses – the Mozilla Django tutorial. Next, let us understand how the project is structured and get an understanding of its source code.

Section D: Project Structure and Source Code

Before we move forward we need to get some idea of how the project is structured. I will explain the same diagrammatically using Fig. 1. From here onward we will focus on how the project is structured. Our primary goal is to understand how the power of htmx is being leveraged in Django and hence some Django specific explanations might be skipped. Feel free to comment below if you want me to explain any section in-depth. To begin with we go to the homepage of the project –

Fig 1: Homepage Structure

The process might look daunting at the beginning but don’t worry. If you are getting confused with the structure remember that most of the functions can be coupled together into one single function. On the other hand, the structure showcased above promotes uniformity and re-usability of codes.

When we navigate to our homepage, our projects maps the request to the start_page() method located at views/ The location of each method is displayed on the top left corner of the blocks. The start_page() method returns the output of two more methods note_add_html() and first_note() coupled together as format_html() and output is displayed as page(). The page() method has been defined in views/ It essentially houses the code of our base template for our HTML pages. If we navigate to our source file we would see this in our start_page() method:

def start_page(request):
    return page(

and if we check our page() method we would notice how htmx is being loaded:

def page(content):
    return HttpResponse(
            '''<!DOCTYPE html>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
        input, textarea {{
            display: block;
<!---- htmx load starts here --->

<script src="" 

<!--- htmx load ends here-->

Create New Note

Similarly, if we follow the diagram from top to bottom we would get a fair idea of how each little method is performing its own task. In the diagram, start_page() first refers to note_add_html(). This method is responsible for displaying a form for users to enter new notes into the Note model. Let us check out the source code for the same:

class NoteCreateForm(ModelForm):
    class Meta:
        fields = ['datetime', 'title', 'text']
        model = Note
        widgets = {
            'text': Textarea(attrs={'rows': 3}),

def note_add_html():
    form = NoteCreateForm()
    return note_form_html(form)

def note_form_html(form):
    return format_html(
    <form hx-post="{url}" hx-swap="outerHTML">
     <input type="submit">

So, node_add_html() is returning note_form_html(). Now our note_form_htm() method is returning HTML with some special htmx attributes. They are:

  • hx-post: This attribute will let the HTML attribute to send POST request to the specified URL. This is basically action and method combined in one simple attribute
  • hx-swap: hx-swap defines how the response will be swapped with respect to the target in our case the <div>. The value passed to this attribute is outerHTML which means that the entire <div> will be swapped with the response

Once the user clicks the submit button the create create_note_hxpost() is called its code is quite self-explanatory. The code snippet is provided below:

def create_note_hxpost(request):
    form = NoteCreateForm(request.POST)
    if form.is_valid():
        note =
        return HttpResponse(format_html('{} {}', note_add_html(), note_html(note)))
    return HttpResponse(note_form_html(form))

Endless Scrolling

Now, the most interesting part about Fig. 1 is the last element; next_html. Our next_html returns a htmx string if the next item exists or return the string The End if no next item exists. Let us look at this special string now and why it is so amazing:

'<div hx-get="note_and_next_hx/1" hx-trigger="revealed" hx-swap="outerHTML">...</div>'
  • hx_get: This attribute will let the HTML attribute to send GET request to the specified URL. What it essentially means is that now <div> is working like <a>
  • hx_trigger: We can use this attribute to trigger a AJAX request. In our case we are using revealed as a trigger event. With this event we can perform lazy loading or according to the docs it is triggered when an element is scrolled into the viewport
  • hx-swap: hx-swap defines how the response will be swapped with respect to the target in our case the <div>. The value passed to this attribute is outerHTML which means that the entire <div> will be swapped with the response

By now you might have noticed how powerful htmx actually is. Our <div> is sending GET requests on-demand and displaying the results. Just one simple line and magic happens. No need to write any additional JavaScript!

Now, you might be wondering what does note_and_next_hx/1 actually do. If we revisit our we would notice that our GET request gets mapped to the method note_and_next_hx(). Let us see its source code to understand it further:

def note_and_next_hx(request, note_id):
    return HttpResponse(note_and_next_html(get_object_or_404(Note, pk=note_id)))

Our method simply returns a HttpResponse against the output of the note_and_next_html() method with the object id as its parameter. And from Fig. 1. we know that the method note_and_next_html() will return the object details and if the next item exists another htmx powered <div> will be created else a plain string ‘The End’ will be showcased.


Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox

We don’t spam! Read our privacy policy for more info.

Leave a Reply

Your email address will not be published. Required fields are marked *