Skip to main content

Command Palette

Search for a command to run...

Implement HTMX modal in Django

Using HTMX we can build modal without the need to setup javascript and its tools (npm, etc) and write it.

Updated
8 min read
G

Finance graduate, payment and billing system builder, htmx enthusiast, and currently working for WeTransfer from Netherlands

In this article, we will use htmx in Django to display data using modal that look like this.

For keeping the article to a manageable size, here are the things that I keep out:

  • setting up htmx and hyperscript.

  • Setting up a tailwindcss. Don't worry, at the end of this I attached the reference to the compiled css result so it is compatible with whatever styling library you use.

  • The dashboard layout template I use throughout this tutorial comes from here, credit to the original author for that. The same thing goes for the modal and the user payments list templates.

  • Fetching the data dynamically on the endpoint. For this article we'll use a mock data of user payments to be rendered on the modal.

  • all other backend stuffs such as authentication, authorization, and error-handling.

The endpoint

Let’s build the endpoint first, for now we just want the endpoint to be able to be called by htmx and returns an html fragment.

# views.py

from django.shortcuts import render
from django.views.generic import TemplateView

# add this endpoint below
def recent_sales_modal(request):
    context = {}
    # this line renders the modal as html fragment
    return render(request, 'pages/recent_sales_modal.html', context)

The modal template that we will return is located on the YOUR_APP_ROOT/pages/ directory and will be called recent_sales_modal.html. For now we keep the context dictionary empty and pass it to the template, we will use it later to pass the data to render. Don’t forget to add the endpoint to the router:

# urls.py

from django.urls import path

from .views import recent_sales_modal # don't forget to import the new endpoint

urlpatterns = [
    ...
    path("recent-sales", recent_sales_modal, name='recent-sales'), # add this line
]

For this article we will differentiate between the path (/recent-sales) and the endpoint (recent_sales_modal) just as a demonstration, but you can definitely give it the same name as the endpoint, if you want.

The modal template

For the modal we want to display information of users that are supposedly made payments recently to us. Let's build the skeletal template first, without the dynamic user payments list, and let’s name it recent_sales_modal.html.

<!--  pages/recent_sales_modal.html  -->

<!-- Modal Overlay -->
<div class="htmx-modal" _='on closeModal remove me'>
  <div class="modal-underlay" _="on click trigger closeModal"></div>
  <!-- Modal Content -->
  <div class="modal-content">
    <!-- Modal Header -->
    <div class="modal-header">
      <h2>Sales made today</h2>
    </div>
    <!-- Modal Body -->
    <div class="modal-body">
      <p>This is the static content of the modal.</p><br>

      <!-- This is where we will add the dynamic data rendering -->            
    </div>
    <!-- Modal Footer -->
    <div class="modal-footer">
      <button class="modal-footer-button" _="on click trigger closeModal">
        Close
      </button>
    </div>
  </div>
</div>

This is how it will look like. Notice that we've achieved

Let’s go through it bit by bit. Both of these lines are special to Django, it is for marking the content

<!-- Modal Overlay -->
<div class="htmx-modal" _='on closeModal remove me'>
  <div class="modal-underlay" _="on click trigger closeModal"></div>
  <!-- Modal Content -->
  <div class="modal-content">

These lines are important for the implementation due to the styling they implement and the _= hyperscript instruction.

Without the styling, the modal will just be added to the page and won’t behave nicely like where it overlays the screen and is centered properly. For the styling, you can view it on the “References” section on the bottom of this article.

The hyperscript is for adding behavior of closing the modal just by clicking on the screen outside the modal or clicking the close button of the modal. The instruction of _="on click trigger closeModal" means that when the modal-underlay is clicked, it will fire a custom event called closeModal which the parent div htmx-modal listens to with the self-explanatory instruction of _='on closeModal remove me' . You can opt out of using hyperscript and use some other way of adding this behaviour, if you don’t feel like using hyperscript for this.

Add the trigger

To tie all of what we just did, we just need to add the link for users to trigger the modal and for that, we need an a tag with these attributes:

<a 
  href='#' 
  hx-get='/recent-sales' 
  hx-target='body'
  hx-swap='beforeend'
>

The hx-get is quite clear on what it does, it instructs htmx to fetch from the /recent-sales endpoint. Replace it if the source endpoint for your modal is different. For the hx-target and hx-swap you’re likely won’t need to change it. hx-target with the value of body means the html fragment returned by the endpoint should be added to the body of the current html page, while the hx-swap tells htmx to append that fragment to the end of body just before the closing tag (hence the beforeend, catchy).

For our case, we want to wrap the link onto the div that displays the recent sales (mocked) amount, like this:

So this is how we achieve that:

<a href='#' hx-get='/detail-modal' hx-target='body' hx-swap='beforeend'>
    <div class="p-4 text-right">
      <p class="block antialiased font-sans text-sm leading-normal font-normal text-blue-gray-600">
      Today's Money
      </p>
      <h4 class="block antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">$53k</h4>
    </div>
</a>

Passing the data to the template

Now we’ve built the endpoint, the template, and the trigger, let’s render the data on the currently skeletal modal. For demonstration purpose, we will use a mocked data to simulate the recent sales data like this:

sales = [
    {
        'user': {
            'image_url': '<https://flowbite.com/docs/images/people/profile-picture-1.jpg>',
            'name': 'Neil Sims',
            'email': 'email@windster.com',                
        },
        'amount': '$320'
    },
    {
        'user': {
            'image_url': '<https://flowbite.com/docs/images/people/profile-picture-3.jpg>',
            'name': 'Bonnie Green',
            'email': 'email@green.com',
        },
        'amount': '$3467'
    },
    {
        'user': {
            'image_url': '<https://flowbite.com/docs/images/people/profile-picture-2.jpg>',
            'name': 'Michael Gough',
            'email': 'email@gough.com',
        },
        'amount': '$97'
    }
]

Remember the empty context dictionary that we pass to the template on the endpoint? Let's make it dynamic-ish now.

# views.py
#--- redacted ---#
    sales = [
        {
            'user': {
                'image_url': 'https://flowbite.com/docs/images/people/profile-picture-1.jpg',
                'name': 'Neil Sims',
                'email': 'email@windster.com',                
            },
            'amount': '$320'
        },
        {
            'user': {
                'image_url': 'https://flowbite.com/docs/images/people/profile-picture-3.jpg',
                'name': 'Bonnie Green',
                'email': 'email@green.com',
            },
            'amount': '$3467'
        },
        {
            'user': {
                'image_url': 'https://flowbite.com/docs/images/people/profile-picture-2.jpg',
                'name': 'Michael Gough',
                'email': 'email@gough.com',
            },
            'amount': '$97'
        }
    ]
    context = {
        'sales': sales
    }
    # this line renders the modal as html fragment
    return render(request, 'pages/recent_sales_modal.html', context)

Now that we've passed the sales data to the template, we can render it on the template. Let’s construct an unordered list of the sales from the endpoint and add it to the inside of .modal-body

<!-- Modal Body -->
<div class="modal-body">
  <p>This is the static content of the modal.</p><br>

  <!-- This is where we will add the dynamic data rendering -->
    <ul role="list" class="modal-body-list">
      {% for record in sales %}
        <li>
          <div class="flex items-center space-x-4">
            <div class="flex-shrink-0">
              <img class="w-8 h-8 rounded-full" src="{{record.user.image_url}}">
            </div>
            <div class="flex-1 min-w-0">
              <p class="text-sm font-medium text-gray-900 truncate">
                {{record.user.name}}
              </p>
              <p class="text-sm text-gray-500 truncate dark:text-gray-400">
                {{record.user.email}}
              </p>
            </div>
            <div class="inline-flex items-center text-base font-semibold text-gray-900">
              {{record.amount}}
            </div>
          </div>
        </li>
      {% endfor %}
    </ul>  
</div>

The modal will look like this now, rendering the mocked data inside the modal.

That is it!

The most exciting thing about htmx is it’s language and framework agnostic, so you can pretty much implement this modal using any backend you want to so long as you do these things:

  • setup htmx as dependency. Optional: for this article we will use hyperscript as well.

  • prepare the html fragment (instead of a whole html page) template

  • hook the html fragment onto an endpoint to render it (don’t forget the routing)

For the first two points you can follow along using any language/backend stack you like, and for the third point you need to adapt it. That is all there is to htmx in essence.

Let me know in the comment what are your thoughts on this, and if I need to go deeper on certain things from this article!


Side note: Notice that for the dynamic html above they are styled using tailwind, but that is because we’ve been using tailwind all this time but it’s just for the skeletal html modal previously we use classes to apply the tailwind tags so when it compiles, it is compiled under all those classes to make them compatible for non-tailwind users as well. For this dynamic part, since this is just for demonstration we keep the original tailwind styling specification.


References

The css for the modal skeleton

minus the dynamic list

.htmx-modal {
  position: fixed;
  inset: 0px;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}

.htmx-modal .modal-underlay {
  position: absolute;
  inset: 0px;
  background-color: rgb(107 114 128 / var(--tw-bg-opacity));
  --tw-bg-opacity: 0.75;
  transition-property: opacity;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}

.htmx-modal .modal-content {
  z-index: 50;
  width: 100%;
  max-width: 28rem;
  overflow: hidden;
  border-radius: 0.375rem;
  --tw-bg-opacity: 1;
  background-color: rgb(255 255 255 / var(--tw-bg-opacity));
  --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
  --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}

@media (min-width: 640px) {
  .htmx-modal .modal-content {
    width: 24rem;
  }
}

@media (min-width: 768px) {
  .htmx-modal .modal-content {
    width: 50%;
  }
}

@media (min-width: 1024px) {
  .htmx-modal .modal-content {
    width: 66.666667%;
  }
}

@media (min-width: 1280px) {
  .htmx-modal .modal-content {
    width: 33.333333%;
  }
}

.htmx-modal .modal-content .modal-header {
  display: flex;
  justify-content: space-between;
  --tw-bg-opacity: 1;
  background-color: rgb(99 102 241 / var(--tw-bg-opacity));
  padding-left: 1rem;
  padding-right: 1rem;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  font-size: 1.125rem;
  line-height: 1.75rem;
  font-weight: 600;
  --tw-text-opacity: 1;
  color: rgb(255 255 255 / var(--tw-text-opacity));
}

.htmx-modal .modal-content .modal-body {
  padding: 1rem;
}

.htmx-modal .modal-content .modal-footer {
  display: flex;
  justify-content: flex-end;
  border-top-width: 1px;
  padding: 0.5rem;
  padding-left: 1rem;
  padding-right: 1rem;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

.htmx-modal .modal-content .modal-footer .modal-footer-button {
  width: 100%;
  border-radius: 0.375rem;
  --tw-bg-opacity: 1;
  background-color: rgb(99 102 241 / var(--tw-bg-opacity));
  padding-left: 0.75rem;
  padding-right: 0.75rem;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
  --tw-text-opacity: 1;
  color: rgb(255 255 255 / var(--tw-text-opacity));
}

@media (min-width: 640px) {
  .htmx-modal .modal-content .modal-footer .modal-footer-button {
    width: auto;
  }
}
Example of htmx modal implementation in Django