Show Django flash messages as toasts with Htmx
Learn how to display Django messages as user friendly toasts with Htmx
Published: 11 Oct, 2024
Updated: 09 Jan, 2025
Django has an amazing messages framework that you can use to notify users about various things that require their attention.
After setting up the framework, you simply display them in templates using whatever layout you want. I used to rely on Bootstrap’s Alert component to display these messages. However, I decided that Toasts are more user friendly because they will subtly pop-up, and dismiss themselves after some time.
In this post, I’ll show you how you can implemented messages as toasts using Htmx.
See this repository for a complete example.
Workflow
You’ll use a view that renders a template containing HTML representing the current messages available in the form of Bootstrap toasts. This is what the template looks like:
{% if messages %}
{% for message in messages %}
<div
class="toast align-items-center"
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div class="d-flex">
<div class="toast-body">{{ message|safe }}</div>
<button
type="button"
class="btn-close me-2 m-auto"
data-bs-dismiss="toast"
aria-label="Close"
></button>
</div>
</div>
{% endfor %}
{% endif %}
You’ll iterate over the list of messages, and output HTML for each toast with the message’s content in the body.
Here’s the view:
from django.contrib import messages
from django_htmx.http import reswap, trigger_client_event
def toast_messages(request):
storage = messages.get_messages(request)
if len(storage) == 0:
response = HttpResponse()
return reswap(response, "none")
response = render(request, "common/toast_messages.html")
return trigger_client_event(response, "toasts:initialize", after="swap")
If no messages are available, you return an empty response and instruct htmx to not swap anything in the DOM. Otherwise, send the rendered toasts template. Also instruct htmx to trigger an event called toasts:initialize after the content has been swapped in.
I’ll make use of the django-htmx helper library in this post. It’s not required to use htmx with Django but it makes interacting with htmx easier thanks to the helper utilities.
Create a container for the toasts in your base template:
<div data-toasts-container hx-get='{% url "core:toast-messages" %}' hx-trigger="toasts:fetch from:body" hx-swap="afterbegin" class="toast-container top-0 end-0 p-3">
{% include "common/toast_messages.html" %}
</div>
When the page loads, render any messages available. Then, htmx will listen for the toasts:fetch event and issues a GET request to the url specified. You want to trigger this event from Django if there are any messages available. Let’s use a middleware that will do it for us after a view returns.
from django.contrib import messages
from django_htmx.http import trigger_client_event
def toaster_middleware(get_response):
def middleware(request):
response = get_response(request)
storage = messages.get_messages(request)
if len(storage) > 0:
response = trigger_client_event(response, "toasts:fetch", after="settle")
return response
return middleware
If there are any messages, the response will cause HTMX to trigger the toasts:fetch event, after which, the above URL will be fetched, dumping the toasts inside the container.
You’ll have to initialize any toasts that appear in the DOM. Create a function to do that:
function initToasts() {
const toastEls = document.querySelectorAll(".toast");
toastEls.forEach((el) => {
const toast = window.bootstrap.Toast.getOrCreateInstance(el);
toast.show();
el.addEventListener("hidden.bs.toast", () => {
el.remove();
});
});
}
This function needs to be called once when the DOM loads, and every time the toasts:initialize event is fired:
document.addEventListener("DOMContentLoaded", () => {
initToasts();
});
document.addEventListener("toasts:initialize", () => {
initToasts();
});
Customizing toasts
The toast component above is bare bones. You will typically want to show different kinds of toasts depending on the message level (or severity). For example, you might want the toast to have a red background for messages with level of error. Let’s use custom template tags to implement those.
Dump the following in a file called toast_tags.py in one of your app’s templatetags package.
from django import template
from django.template.base import token_kwargs
register = template.Library()
class ToasterNode(template.Node):
def __init__(self, nodelist, tag_kwargs):
self.nodelist = nodelist
self.tag_kwargs = tag_kwargs
def get_level(self, context):
level_var = self.tag_kwargs["level"]
level = ""
if level_var.is_var:
level = level_var.resolve(context)
else:
level = level_var.var
return level
def build_context(self, context):
level = self.get_level(context)
color_class = f"text-bg-{level}"
close_button_class = ""
if level == "info":
close_button_class = "btn-close-dark"
elif level == "success":
close_button_class = "btn-close-white"
elif level == "warning":
close_button_class = "btn-close-dark"
elif level == "danger":
close_button_class = "btn-close-white"
else:
close_button_class = ""
return {"color_class": color_class, "close_button_class": close_button_class}
def render(self, context):
toast_ctx = self.build_context(context)
with context.push({"toast": toast_ctx}):
return self.nodelist.render(context)
@register.tag("toaster")
def do_toaster(parser, token):
bits = token.split_contents()
bits_without_tagname = bits[1:]
tag_kwargs = token_kwargs(bits_without_tagname, parser)
nodelist = parser.parse(("endtoaster",))
parser.delete_first_token()
return ToasterNode(nodelist=nodelist, tag_kwargs=tag_kwargs)
This tag can be used by wrapping the HTML element between {% toaster %}
and {% endtoaster %}
tags.
Now, update your toast_messages.html file to make use of your new tag:
{% load toast_tags %}
{% if messages %}
{% for message in messages %}
{% toaster level=message.level_tag %}
<div
class="toast align-items-center {{ color_class }}"
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div class="d-flex">
<div class="toast-body">{{ message|safe }}</div>
<button
type="button"
class="btn-close me-2 m-auto {{ close_button_class }}"
data-bs-dismiss="toast"
aria-label="Close"
></button>
</div>
</div>
{% endtoaster %}
{% endfor %}
{% endif %}
Now the toast will be properly formatted according to the level of the message.
Note that I also rename the default error level tag to danger.
Conclusion
Django and Htmx make it simple to notify users of important messages via toasts. All you have to do is render the HTML, and then use Htmx to fetch any new messages after every AJAX request. Try it out in your project now.