How to add a contact form using Django and htmx

In this short guide, you will learn how to add a contact form to your app. This guide is tailored for Vanty Starter kit users, but the concepts here may also be applied to other Django apps.

The contact will be form to capture basic information about potential clients and send it to the configured inbox. We will use the htmx library to make the UI modern/responsive.


  • Vanty Starter Kit - This guide makes reference to libraries and modules that are already bundled with the starter kit. This includes:
    • django_htmx
    • emailing utilities and templates
    • base templates for the contact page

Other than that, you should be able to follow along and apply the same principles to your project. Let's get started.

Set up the views

from apps.common.emails import send_generic_email

def contact_form_view(request):

    template_name = "web/contact_page.html"
    if request.htmx:
        template_name = "web/htmx_contact_form.html"
    if request.method == "GET":
        form = ContactForm()
        context = {"contact_form": form}
        return render(request, template_name, context)

    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            response = render(request, "web/htmx_contact_confirm.html")
                {"type": "success", "message": "Message sent successfully"},

            return response
            return render(
                context={"contact_form": form}


This is a simple view that either returns a blank form for GET requests or a validated form for POST requests.

  • if request.htmx : - set a different template name when the request is htmx. The htmx attribute is added by the django_htmx library middleware which is bundled with the starter kit.
  • send_generic_email - we call this method to send an email to the DEFAULT_EMAIL address included in config/settings/ Please see the docs on more info on how to set up emails.
  • trigger_client_event - successfully validated forms will trigger a client event of type 'notice' to give the user some feedback.

Add the forms and urls

class ContactForm(forms.Form):
    full_name = forms.CharField(max_length=400)
    email = forms.EmailField(required=True)
    message = forms.CharField(
            {"rows": 4, "placeholder": "Tell us more about your product"}

    def clean_email(self):
        email = self.cleaned_data["email"]
        # you can black list certain emails here, e.g. span accounts etc
        for domain in settings.ACCOUNT_EMAIL_DOMAIN_BLACKLIST:
            if domain in email:
                raise forms.ValidationError(
                    f"{email} has been blacklisted. "
                    f"Please use a valid email or get in touch with us"
        return email


urlpatterns = [
  path("contact-us/", contact_form_view, name="contact-form"),


The form is a standard django form with a small modification

def clean_email(self) - In addition to validating the email field, the clean email method also checks that the email is not in the list blacklisted emails. This is important if you expect a lot of traffic from this page. If you are still getting a lot of span consider using the django_ratelimit library on the view.

Add the form templates


{% extends "layouts/landing.html" %}{% load static i18n vanty_tags %}
{% block title %} Vanty Demo | Contact {% endblock %}{% endblock %}
{% block theme %}data-theme="light"{% endblock %}
{% block promo_banner %}{% endblock %}
{% block page_content %}
  <div class="bg-white container md:mx-auto max-w-[65ch] my-2 p-2 md:p-0">
  <div id="contact-form">
    <div class="overflow-hidden">
      <section class="relative bg-white" aria-labelledby="contact-heading">
        <div class="absolute w-full h-1/2 bg-warm-gray-50" aria-hidden="true"></div>
          <div class="relative bg-white">
            <h2 id="contact-heading" class="sr-only">Contact us</h2>
            {% include "web/htmx_contact_form.html" %}
  {% include "shared/footer/main_footer.html" with is_footer_white=True  %}
{% endblock %}


{% load crispy_forms_tags heroicons %}

<div class="flex justify-between items-center">
  <h3 class="text-lg font-medium text-warm-gray-900">Get in touch</h3>
<div class="mt-6 px-3">
  <form novalidate 
        hx-post="{% url "contact-form" %}"  
    {% csrf_token %}
    {{ contact_form|crispy }}
    <div class="sm:col-span-2 sm:flex sm:justify-end mt-5">
      <button type="submit" class="btn btn-sm">


  • web/ - both the main contact page and form are in a folder called web. You can place them where ever you prefer. Just remember to update the view accordingly.
  • {% extends "layouts/landing.html" %} - the contact page extends the landing.html template. This template already includes the dependencies you need such as the htmx library.
  • <form novalidate . .. - pay attention to the form attributes. Notice how the action attribute is replaced by hx-post ="". This tells htmx where to post the contact form to. The hx-target="#contact-form" tag instructs htmx to swap out the contents of the element with id #contact-form with the response from the server.

Email templates


{% extends "templated_email/base_email.html" %}

{% block content %}

  <p>Message from {{full_name}}, {{email}}</p>

  <p>{{ message }}</p>

{% endblock %}
{% block button %}
  <a href="{{ domain | safe }}" target="_blank">Dashboard </a>
{% endblock %}


{% load i18n %}

{% block subject %}
  {% blocktrans trimmed context "New Contact" %}
    Contact Email
  {% endblocktrans %}
{% endblock %}

{% block plain %}

Message from {{full_name}} {{email}}

{{ message }}

{{ domain }}

{% blocktrans trimmed context "Base email text" %}
This is an automatically generated e-mail, please do not reply.
{% endblocktrans %}
{% blocktrans trimmed context "Base email footer" %}
Sincerely, {{ site_name }}
{% endblocktrans %}

{% endblock %}

{% block html %}
{% include 'templated_email/contacts/contact.html' %}
{% endblock %}


  • Finally we add the email templates. Be sure to include both the plain text and html template for emails. The base templates have already been set up for you.


That's it. The emails should be sent to the default email you included in your settings. To test this is working, check the development mailbox (mailhog) if you are using the docker.