Bootstrap 5 for Django Developers: Build Responsive UIs Without Writing Custom CSS
Bootstrap 5 for Django Developers: Build Responsive UIs Without Writing Custom CSS
If you've built Django apps before, you know the backend logic comes together quickly with its models, views, URLs and templates. The part that often slows things down is making it look presentable. Writing custom CSS from scratch takes time, and utility-first frameworks like Tailwind have a learning curve. Bootstrap 5 hits a practical middle ground: a mature component library with sensible defaults that you can drop into any Django project and start using immediately.
This tutorial walks through integrating Bootstrap 5 into a Django project, covering layout, components, and a few patterns that work especially well with Django's templating system.
Prerequisites
- Python 3.10+ and Django 4.x installed
- A basic Django project with at least one app
- Familiarity with Django templates
No Node.js or build tools required, we'll load Bootstrap via CDN to keep things simple.
1. Set Up Your Django Project
If you're starting from scratch:
python -m venv venv
source venv/bin/activate
pip install django
django-admin startproject mysite
cd mysite
python manage.py startapp core
Register core in INSTALLED_APPS inside mysite/settings.py:
INSTALLED_APPS = [
...
'core',
]
2. Create a Base Template with Bootstrap 5
Django's template inheritance makes it straightforward to add Bootstrap once and have every page inherit it. Create a templates/ directory at the project root and add a base.html:
mysite/
├── templates/
│ └── base.html
├── core/
└── mysite/
Update TEMPLATES in settings.py so Django finds the root-level templates folder:
TEMPLATES = [
{
...
'DIRS': [BASE_DIR / 'templates'],
...
},
]
Now create templates/base.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}My Site{% endblock %}</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">MyApp</a>
</div>
</nav>
<main class="container my-4">
{% block content %}{% endblock %}
</main>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmWt/eEEoJTMQMRoEb+E2G8qPQ6S"
crossorigin="anonymous"
></script>
</body>
</html>
The {% block content %} tag is where child templates inject their content.
3. Build a Child Template
Create templates/core/home.html:
{% extends "base.html" %}
{% block title %}Home | MyApp{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<h1 class="mb-3">Welcome to MyApp</h1>
<p class="lead">This page is built with Bootstrap 5 and Django templates.</p>
<a href="/items/" class="btn btn-primary">Browse Items</a>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Quick Stats</h5>
<p class="card-text">Total items: <strong>{{ item_count }}</strong></p>
</div>
</div>
</div>
</div>
{% endblock %}
Wire it up in core/views.py:
from django.shortcuts import render
def home(request):
return render(request, 'core/home.html', {'item_count': 42})
And in mysite/urls.py:
from django.urls import path
from core import views
urlpatterns = [
path('', views.home, name='home'),
]
4. Key Bootstrap Layout Concepts
The Grid System
Bootstrap uses a 12-column grid. Columns are defined with classes like col-md-6 (half-width on medium screens and up). The grid handles responsive stacking automatically. Columns stack vertically on small screens without any media query work on your end.
<div class="row">
<div class="col-md-4">Sidebar</div>
<div class="col-md-8">Main content</div>
</div>
Containers
Use container for a centered, max-width wrapper. Use container-fluid for full-width layouts.
Spacing Utilities
Bootstrap's spacing utilities (m-, p-, mt-, mb-, py-, etc.) handle margins and padding with a consistent scale. For example, my-4 adds vertical margin equal to 1.5rem. This replaces most one-off CSS rules for spacing.
5. Django Forms with Bootstrap Styling
Django's form rendering doesn't add Bootstrap classes by default. There are two common approaches:
Option A: Manual template rendering
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="id_username" class="form-label">Username</label>
<input type="text" name="username" id="id_username" class="form-control">
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
Option B: Use django-crispy-forms with the Bootstrap 5 template pack
pip install crispy-bootstrap5
# settings.py
INSTALLED_APPS += ['crispy_forms', 'crispy_bootstrap5']
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5'
CRISPY_TEMPLATE_PACK = 'bootstrap5'
Then in your template:
{% load crispy_forms_tags %}
{{ form|crispy }}
crispy-forms automatically adds the correct Bootstrap classes to your form fields, which is a significant time-saver on form-heavy apps.
6. Displaying Django Messages with Bootstrap Alerts
Django's messages framework pairs well with Bootstrap's alert component. Add this to your base.html inside <main>:
{% 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 %}
Map Django message levels to Bootstrap colors in settings.py:
from django.contrib.messages import constants as messages
MESSAGE_TAGS = {
messages.DEBUG: 'secondary',
messages.INFO: 'info',
messages.SUCCESS: 'success',
messages.WARNING: 'warning',
messages.ERROR: 'danger',
}
7. Useful Bootstrap Components for Django Apps
Cards - great for listing model instances:
{% for item in items %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ item.name }}</h5>
<p class="card-text">{{ item.description }}</p>
<a href="{% url 'item_detail' item.pk %}" class="btn btn-sm btn-outline-primary">View</a>
</div>
</div>
{% endfor %}
Badges - useful for status fields:
<span class="badge bg-success">{{ item.status }}</span>
Modals - confirmation dialogs before deletes:
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirmDelete">
Delete
</button>
<div class="modal fade" id="confirmDelete" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">Are you sure you want to delete this item?</div>
<div class="modal-footer">
<form method="post" action="{% url 'item_delete' item.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
8. When to Move Beyond CDN
The CDN approach is fine for internal tools, prototypes, and small apps. When you need to:
- Customize Bootstrap's color palette or spacing scale
- Remove unused CSS for performance
- Integrate a design system
...you'll want to set up a build pipeline with sass and optionally django-compressor or Vite. That's a separate topic, but the templating patterns above carry over completely.
Takeaways
- A single
base.htmlwith Bootstrap's CDN link covers all pages via Django template inheritance. Set it up once and forget it. - Bootstrap's 12-column grid and spacing utilities handle 80% of layout needs without custom CSS.
django-crispy-formswith the Bootstrap 5 pack is worth adding to any form-heavy project.- Map Django's message levels to Bootstrap alert variants in
settings.pyfor consistent flash messages across the app. - Cards, badges, and modals cover the most common UI patterns for data-driven Django apps.
Bootstrap 5 isn't flashy, but it's reliable and well-documented. For Django developers who want professional-looking UIs without a dedicated frontend layer, it's a practical default choice.