Goal
The goal is to install the Bootstrap 5 frontend framework and integrate it with Django Templates and Django Forms.

Installing Bootstrap via CDN
This is the perfect opportunity to set up a template structure. I tend to start with a root base.html
that includes template code that will apply to all pages of the app.
<!doctype html>
<html lang="en">
<head>
{% block meta %}
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
{% endblock %}
<title>{% block title %}lith{% endblock %} | lith</title>
{% block css %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7"
crossorigin="anonymous">
{% endblock %}
</head>
<body>
{% include "lith/partials/messages.html" %}
{% block content %}
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"
integrity="sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq"
crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
This base template includes the following blocks:
meta
- which includes default meta tags for responsive displaytitle
- gives a page a default titlecss
- includes the vendor stylingjs
- includes vendor scriptscontent
- an empty block, as we want to allow each sub-directories to define their ownbase.html
and content structure if necessary
The login and dashboard pages can now extend the base template:
# src/lith/templates/lith/auth/login.html
{% extends "lith/base.html" %}
{% block title %}login{% endblock %}
{% block content %}
<form action="{% url "lith:login" %}" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Login">
</form>
{% endblock %}
# src/lith/templates/lith/dashboard/dashboard.html
{% extends "lith/base.html" %}
{% block title %}dashboard{% endblock %}
{% block content %}
{% include "lith/partials/navbar.html" %}
<h1>Hello, {{ email }}!</h1>
{% endblock %}
Integrate Bootstrap Alerts to Django Messages
I like using dismissible alerts when displaying Django Messages. They can be integrated by updating our messages partial:
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
A small gotcha is that writing an error message in Django uses the 'error'
tag, where Bootstrap errors uses the 'danger'
class. The rest of the message levels match up nicely, but we’ll have to update the default mappings:
# src/lith/config/settings.py
# Custom message tags
# https://docs.djangoproject.com/en/5.1/ref/contrib/messages/#message-tags
MESSAGE_TAGS = {
messages.ERROR: "danger",
}
Using Tabler
Tabler is a free and open-source Bootstrap admin template that looks quite good in my opinion. Getting started is as simple as updating the Bootstrap CSS and JS links to use the Tabler version:
{% block css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.1.1/dist/css/tabler.min.css" />
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.1.1/dist/js/tabler.min.js"></script>
{% endblock %}
Updating the Login Page and Form
The template change is relatively straightforward. Using Tabler’s login demo as reference, I stripped away the components that I didn’t need and came out with the following:
{% extends "lith/base.html" %}
{% load crispy_forms_tags %}
{% block title %}login{% endblock %}
{% block content %}
<div class="page page-center">
<div class="container container-tight py-4">
<div class="card card-md">
<div class="card-body">
{% include "lith/partials/messages.html" %}
<h2 class="h2 text-center mb-4">Login to your account</h2>
{% crispy form %}
</div>
</div>
</div>
</div>
{% endblock %}
In order for the {% crispy form %}
tag to render properly, I have to update the LoginForm
class:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django import forms
from django.urls import reverse
class LoginForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_action = reverse("lith:login")
self.helper.add_input(Submit("submit", "Login", css_class="btn btn-primary w-100"))
email = forms.EmailField(label="Email")
password = forms.CharField(label="Password", widget=forms.PasswordInput)
With not a lot of code change, I got a clean login page that’s easy on the eyes:

Updating the Dashboard Page
The dashboard update is even simpler, I just had to pick and choose from the dashboard demo and copy and paste what I needed. I split out the components for readability, but here’s the new dashboard template:
{% extends "lith/base.html" %}
{% block title %}dashboard{% endblock %}
{% block content %}
{% include "lith/dashboard/partials/top-nav.html" %}
{% include "lith/dashboard/partials/bottom-nav.html" %}
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container-xl">
{% include "lith/partials/messages.html" %}
<div class="row g-2 align-items-center">
<div class="col">
<!-- Page pre-title -->
<div class="page-pretitle">Overview</div>
<h2 class="page-title">Dashboard</h2>
</div>
<!-- Page title actions -->
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<span class="d-none d-sm-inline">
<a href="#" class="btn btn-1"> New Action </a>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<h1>Hello, {{ email }}!</h1>
</div>
</div>
</div>
{% endblock %}

Conclusion
In this lab, I installed Bootstrap 5 and integrated it with Django Templates and Forms. I also added the Tabler Admin Template in order to quickly get a visually pleasant app without spending much time in design.
The source is available at the feature/007-install-bootstrap
branch.