Create a contact form with Django Crispy Forms and Bootstrap 4


One of the most important skills you can have as a web developer is the ability to quickly build a form. It could be argued that most web development consists of ordering a list of forms and making them play nice together.

On this post I will teach you how to build a simple contact form with Django and Bootstrap.

Instagram greets you with a Django and React form

Quick overview of HTML forms

<form action="/register" method="POST">
<input type="text" id="name" name="name" placeholder="name"><br>
<input type="email" id="email" name="email" placeholder="email"><br>
<input type="password" id="password" name="password" placeholder="password"><br>
<input type="submit" value="Next">

This form will hit the endpoint /register with a POST request when we press the 'Next' button.

HTML inputs can have different types and they do some basic validation depending on their type. The type of a field can also affect its behaviour, for example password fields will hide their text by default.

Setting up the project

Let's create a virtual environment to install our Python packages. If you have never done this you will have to install the virtualenv tool like this first:

$ sudo apt install virtualenv

We can now create an isolated copy of Python to prevent messing with the system.

$ virtualenv -p /usr/bin/python3.8 venv
$ source venv/bin/activate

You may have to replace python3.8 above with your system's version, if in doubt you can simply check which versions you have available:

$ ls /usr/bin/python*

If you ever want to exit this environment you can simply type deactivate. Let's install some packages and create our project.

$ pip install django django-crispy-forms
$ django-admin startproject project
$ cd project
$ ./ startapp contact_form
$ mkdir -p contact_form/templates/contact_form


# 3rd party

    #other stuff


Creating a message model


class Message(models.Model):
    email = models.EmailField(max_length=254)
    message = models.TextField(max_length=300)

If you are using the default database engine (SQLite) max_length is completely up to you, since SQLite doesn't enforce text length. It's a different story with other engines such as MySQL and PostgreSQL (the one that powers this site), which I will show you in the future.

$ ./ makemigrations
$ ./ migrate

You should see something similar to this:

Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK

Introducing the Forms API and the messages framework

HTML forms are very easy to write once you learn the basics. However, it's also very easy to make mistakes when you write HTML manually. This is not a big deal when the changes are only visual, but in the case of forms our application will simply stop working.

Other than that, HTML only does some mild validation on the browser. There is nothing preventing a malicious user from sending unwanted data to our app.

By using Django Forms with Crispy Forms, we will kill two birds with one stone by rendering the form with Bootstrap 4 and validating the data.

Once the form has been validated and the data has been saved, we will use Django's messages framework to display a success message.

Writing a Django form using Django Crispy Forms


from django import forms

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Field

class ContactForm(forms.Form):
def __init__(self, *args, **kwargs):
super(ContactForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()

self.helper.form_method = 'post'
self.helper.form_action = '/contact'

self.helper.add_input(Submit('submit', 'Submit'))

email = forms.EmailField(max_length=254)
message = forms.CharField(max_length=254, widget=forms.Textarea)

Creating a form with Django Crispy Forms revolves around its FormHelper class and some template tags that we will see later.

For this example, we have set the form_method and form_action attributes, rendering a POST form that sends data to the /contact endpoint. We then add email and message fields.

Using a FormView to validate the form and notify the user


from django.contrib import messages
from django.views.generic.edit import FormView

from .forms import ContactForm
from .models import Message

class ContactFormView(FormView):
template_name = 'contact_form/contact.html'
form_class = ContactForm
success_url = '/contact'

def form_valid(self, form):
email = form.cleaned_data['email']
message = form.cleaned_data['message']

message = Message(email=email, message=message)

messages.success(self.request, 'Message received 👽')

return super().form_valid(form)

To wire a form to a FormView you have to set the form_class attribute.

Once we validate the data, we save it to our database as a ContactMessage object, and we display a notification using the messages framework.

By default, form_valid simply redirects to success_url. By overwriting form_valid(), Django will run our code first and then redirect to success_url.

How does form_valid() work?

Generic views such as FormView make use of a type of multiple inheritance called a mixin. As the name indicates, a mixin is used to mix in some extra functionality into a class.

FormViews mix in some template response logic and a view called BaseFormView. This view mixes in some form logic and a view called ProcessFormView, which finally validates the form with is_valid().

The beautiful source code makes it a bit easier to understand:


class ProcessFormView(View):
    """Render a form on GET and processes it on POST."""
    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        return self.render_to_response(self.get_context_data())

    def post(self, request, *args, **kwargs):
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
            return self.form_invalid(form)

    # PUT is a valid HTTP verb for creating (with a known URL) or editing an
    # object, note that browsers only support POST for now.
    def put(self, *args, **kwargs):
        return*args, **kwargs)

class BaseFormView(FormMixin, ProcessFormView):
    """A base view for displaying a form."""

class FormView(TemplateResponseMixin, BaseFormView):
    """A view for displaying a form and rendering a template response."""

Adding a contact form URL and a form template


from django.urls import path

from . import views

app_name = 'contact_form'

urlpatterns = [
path('contact', views.ContactFormView.as_view(), name='contact'),


from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('', include('contact_form.urls')),


<link rel="stylesheet" href="" integrity="sha256-gvEnj2axkqIj4wbYhPjbWV7zttgpzBVEgHub9AAZQD4=" crossorigin="anonymous" />

<link rel="stylesheet" href="" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

{% load crispy_forms_tags %}

{% if messages %}

{% for message in messages %}
<div class="alert alert-success col-lg-6 col-sm-12 col-xs-12 text-center mx-auto" role="alert">
<li class="{{ message.tags }}">{{ message }}</li>
{% endfor %}

{% endif %}

<div class="col-lg-6 col-md-8 mx-auto">
{% crispy form %}

Whenever we use custom template tags we need to load them first with {% load custom_tags %}.

We will display our notification messages as a list. Usually only one message will be shown, but this makes sure other scenarios don't break our layout.

Message tags are string constants that are similar to log levels: DEBUG, INFO, SUCCESS, WARNING and ERROR.

Remember the HTML form I showed you in the beginning? With Crispy Forms, all you need to render a similar form is this template tag:

{% crispy form %}

Other than that, I have wrapped the form in a div with some Bootstrap classes that limit its width and center it.


Using the form and why I reset CSS

That's it! Use your new form at

You are probably wondering what the first import does in the template above.

The reason I imported that CSS sheet is because default CSS is inconsistent across browsers, and it can lead to awkward results when you lay out your content.

Fortunately a developer called Eric Meyer spent some time working on a CSS sheet that solves this, by doing what is known as a CSS reset.

Before reset.css, with some hidden CSS styles.

After reset.css, with the hidden CSS now gone.