Jake Groszewski

Blog

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.html with 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-forms with the Bootstrap 5 pack is worth adding to any form-heavy project.
  • Map Django's message levels to Bootstrap alert variants in settings.py for 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.