Adding Bootstrap to Django means getting two files in front of the browser, bootstrap.min.css and bootstrap.bundle.min.js, and linking both from a base template. The files can load from a CDN or from static files you host yourself. The django-bootstrap5 package can supply them too.

Bootstrap is about as common as CSS frameworks get. W3Techs measured it on 74.8% of websites with a detectable CSS framework in October 2026. Django has no opinion on it and serves local copies of the files through its staticfiles app.

One warning before you copy anything from older tutorials. W3Schools and a DEV Community post both install django-bootstrap-v5, and its last release (1.0.11, February 2022, per piwheels) requires Django below 5.0. W3Schools’ own pip output shows that requirement. The maintained package is django-bootstrap5, and its changelog lists support for Django 6.0 and 6.1.

Which method should you use to add Bootstrap to Django?

CDN for prototypes, local static files for anything you deploy. The django-bootstrap5 package makes sense only if you want its form and message tags, and npm starts to pay off once you compile Sass.

Django doesn’t care which one you pick. To it, the Bootstrap framework is just compiled CSS and JavaScript that needs delivering to the browser.

MethodSetupWorks offlineBest fit
CDN linkTwo tags in base.htmlNoPrototypes, demos
Local static filesDownload, copy into static/YesProduction sites
django-bootstrap5pip install, INSTALLED_APPSNo (CDN by default)Projects using its form tags
npmnpm install, then copy or compileYesSass customization

CDN, local files, package and npm compared

CDN link

Nothing to store in the repository, and a version change is one line. The catch is a third-party host sitting in your page load. If that host is unreachable, the page loses its styling.

Local static files

Styles load from your own domain, so a blocked CDN can’t break the page, and the exact version is committed with the code. Every upgrade means downloading and copying files again, which gets old.

django-bootstrap5 package

It comes with template tags for forms and messages (buttons as well) and handles the CSS and JS tags for you. It’s also one more dependency that has to keep pace with both Django and Bootstrap.

npm

You get the Sass source files and a version pinned in package.json. You also get a Node toolchain inside a Python project, plus a copy or compile step before Django can serve anything.

Which Bootstrap version to load

Bootstrap v5.3.8 shipped on 25 August 2025, and the Bootstrap blog described it as the planned last patch before v5.4.0. Check getbootstrap.com for the current release before you copy a snippet, because pasted version numbers go stale quietly.

Bootstrap 5.3 behaves the same through the CDN, local files or either package. Tutorials for Bootstrap 4 and older pair it with jQuery, and none of that applies to version 5. Bootstrap 6 alpha, first released 8 October 2026, is a different story: its docs load the JS bundle as a module and replace Popper with Floating UI, so 5.3 snippets do not carry over.

Bootstrap 5 dropped the jQuery dependency, which removed 85KB of minified JavaScript, per InfoQ’s 2020 coverage of the alpha. Skip any tutorial step that adds a jQuery script tag for Bootstrap 5.

Stay on 5.3 for anything you ship.

Where is web design headed next?

Discover the latest web design statistics: industry growth, design trends, technology adoption, and insights defining the future of the web.

Explore the Data →

Django static file settings that Bootstrap depends on

Local Bootstrap files only work if Django can find them. That takes django.contrib.staticfiles in INSTALLED_APPS, a STATIC_URL, and a STATICFILES_DIRS entry pointing at your project’s static folder. The CDN route skips all of it.

Django runs on the backend, so a stylesheet reaches the browser only when Django can find that file through these settings.

With django.contrib.staticfiles installed, the development server discovers and serves static files. STATIC_URL is the public URL prefix, usually static/. STATICFILES_DIRS adds extra folders for Django to search besides each app’s own static directory. STATIC_ROOT only matters at deploy time, since it’s the folder collectstatic copies files into.

INSTALLED_APPS = [

# ...

"django.contrib.staticfiles",
]

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

Put Bootstrap in a namespaced subfolder instead of dumping files straight into static/.

static/
bootstrap/
css/bootstrap.min.css
js/bootstrap.bundle.min.js

The Django settings reference lists STATIC_URL with a default of None, so the setting is required.

With staticfiles installed and DEBUG set to True, runserver serves these files without extra URL configuration. Set DEBUG to False and the same page loses its styles. That’s why a page that looks fine locally can break after deployment.

Installing Bootstrap in Django: CDN, local files and django-bootstrap5

Whichever method you choose, base.html ends up referencing one Bootstrap stylesheet and one JavaScript bundle. Only the source of those files changes.

Adding Bootstrap with the CDN

The CDN route is two tags and no Django settings.

Paste the stylesheet into head and the script before the closing body tag (5.3.3 is shown here, so swap in the version and hash from getbootstrap.com):

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>

The Bootstrap docs recommend jsDelivr and an integrity attribute on every CDN tag. The hash pins the exact file, so changing the version in the URL without updating the hash makes the browser block it.

Generate a fresh hash with openssl dgst -sha384 -binary bootstrap.min.js | openssl base64 -A, the command the docs give.

Adding Bootstrap with local static files

Download the compiled CSS and JS from getbootstrap.com, unzip the archive, and copy two files into your static folder.

  • css/bootstrap.min.css
  • js/bootstrap.bundle.min.js

Keep them under static/bootstrap/ so the paths match the folder tree from the settings section, and make sure STATICFILES_DIRS points at the folder that holds bootstrap/.

Copy the .map files next to them too. The minified files reference their source maps, so without them the browser’s dev tools request files that return 404.

Adding Bootstrap with the django-bootstrap5 package

  1. Run pip install django-bootstrap5.
  2. Add "django_bootstrap5" to INSTALLED_APPS.
  3. Put {% load django_bootstrap5 %} at the top of the template, then use {% bootstrap_css %} and {% bootstrap_javascript %}.

Two similarly named packages exist, and tutorials mix them up. W3Schools and a DEV Community post install django-bootstrap-v5, while the maintained package is django-bootstrap5 from Zostera.

PackageLoad tagINSTALLED_APPS entryStatus
django-bootstrap5{% load django_bootstrap5 %}django_bootstrap5Supports Django 6.0 and 6.1
django-bootstrap-v5{% load bootstrap5 %}bootstrap5Last release February 2022, Django below 5.0

Piwheels dates the last django-bootstrap-v5 release (1.0.11) to 10 February 2022. The pip output in W3Schools’ tutorial shows that release requiring Django below 5.0, so it cannot install alongside Django 5.0 or newer.

The django-bootstrap5 changelog records Django 6.0 support in release 25.3 (November 2025) and Django 6.1 support in release 26.3 (August 2026). Release 24.2 (April 2024) updated Bootstrap to 5.3.3, and release 26.3 moved the package’s default to 5.3.8.

By default the package’s CSS and JavaScript tags render CDN URLs, so this route inherits the CDN’s offline limit. If you want local files instead, the BOOTSTRAP5 dict in settings.py overrides css_url and javascript_url.

The docs’ settings page still shows a 5.2.0 URL in its default example, even though the changelog records 5.3.8 as the default since release 26.3. Check the rendered page source to see what your installed version actually loads.

Adding Bootstrap with npm

npm adds a Node toolchain to a Python project, so it earns its place only when you compile Sass. For plain CSS and JS it is the local-files route with extra steps.

Install with npm install bootstrap@5.3. The files land in node_modules/bootstrap/dist/, and Django can read them there through a prefixed STATICFILES_DIRS entry:

STATICFILES_DIRS = [
BASE_DIR / "static",
("bootstrap", BASE_DIR / "node_modules" / "bootstrap" / "dist"),
]

The catch is that node_modules has to exist wherever collectstatic runs, so the deploy step needs npm install first.

How to set up the base template for Bootstrap

Most of the base template is ordinary Django template inheritance. The Bootstrap parts are a viewport meta tag, the stylesheet link in head and the JavaScript bundle before the closing body tag, with blocks in between for child pages to fill.

The viewport meta tag tells mobile browsers to render at the device width, and without it Bootstrap’s responsive breakpoints don’t fire the way the docs show.

  1. Create templates/base.html and confirm that TEMPLATES[“DIRS”] includes BASE_DIR / "templates".
  2. Add {% load static %} on the first line (skip it for the CDN route).
  3. Add the viewport meta tag and a title block inside head.
  4. Add the Bootstrap stylesheet link in head, using the tag from the method you chose.
  5. Wrap a content block in a div with the container class inside body.
  6. Add the JavaScript bundle just before the closing body tag.
  7. Add an extra_js block after it for page-specific scripts.
  8. Extend the base in a child template with {% extends "base.html" %}, run runserver, and confirm both Bootstrap files return 200 in the browser’s network tab.
{% load static %}
<!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 rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
<script src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

Test with one visible component first. A Bootstrap navbar works well because it tests the stylesheet and the JS bundle together, and the responsive collapse on top of that.

A 404 on either file means the path or STATICFILES_DIRS is wrong.

Bundle or non-bundle JavaScript

Use bootstrap.bundle.min.js unless you already load Popper yourself.

The bundle includes Popper, so dropdowns, tooltips and popovers work with one file. The plain bootstrap.min.js leaves Popper out, so you load @popperjs/core first. The Bootstrap 5.3 download page lists Popper 2.11.8 for this setup.

The bundle also fits django-bootstrap5. Its JavaScript tag renders the bundled file, and the docs say to load files manually if you need a separate Popper.

The failure mode is quiet. Plain bootstrap.js without Popper leaves dropdowns, tooltips and popovers broken, while modals and alerts keep working, because only the positioned components depend on Popper.

Place the script before the closing body tag, or in head with defer.

Styling Django forms with Bootstrap classes

Django renders form fields as plain inputs, so nothing looks like Bootstrap until each widget gets the right class. That means form-control on text inputs, form-select on selects and form-check-input on checkboxes. You can set them by hand in the widget attributes, use django-widget-tweaks, use crispy-bootstrap5, or let the django-bootstrap5 form tag do it.

Bootstrap form markup also expects a label above each input and a feedback block for errors. That is the part hand-written templates tend to skip.

Setting classes by hand in forms.py is verbose, but it adds no dependency.

class ContactForm(forms.Form):
email = forms.EmailField(
widget=forms.EmailInput(attrs={"class": "form-control"})
)
topic = forms.ChoiceField(
choices=TOPICS,
widget=forms.Select(attrs={"class": "form-select"}),
)

With django-widget-tweaks, add widget_tweaks to INSTALLED_APPS and set classes in the template instead of the form class.

{% load widget_tweaks %}

{% render_field form.email class="form-control" %}

The README warns that chained filters apply with the leftmost one winning, which surprises people who expect left-to-right order. Its add_error_class filter adds a class such as is-invalid only when validation failed.

crispy-bootstrap5 installs next to django-crispy-forms. Add both crispy_forms and crispy_bootstrap5 to INSTALLED_APPS, then set CRISPY_ALLOWED_TEMPLATE_PACKS and CRISPY_TEMPLATE_PACK to "bootstrap5".

Then render a form with {{ form|crispy }}. Since version 2.0, django-crispy-forms ships template packs as separate packages, so the core library alone produces no Bootstrap markup.

The crispy docs also state that crispy-forms includes no static files, so base.html still has to load Bootstrap. For Bootstrap 5 features the pack adds FloatingField, BS5Accordion and Switch as layout objects.

If django-bootstrap5 already supplies your CSS tags, its form tag costs nothing extra.

{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button button_type="submit" content="Save" %}

Manual classes suit one or two forms. Once many forms share a layout, crispy-bootstrap5 is the one I’d pick, and widget-tweaks fits when the markup lives with a front-end developer.

Keep a visible label on every field, because accessible forms depend on labels rather than placeholders. Bootstrap’s floating labels keep a real label element in the markup.

Customizing Bootstrap with Sass in a Django project

Compile your own Bootstrap CSS when theme colors and breakpoints have to change at the source, spacing included. A small tweak needs only a CSS file loaded after Bootstrap’s stylesheet.

Overriding variables with Sass

Install the Sass source with npm install bootstrap sass, then create scss/custom.scss, which sets your values before pulling in Bootstrap:

@import "../node_modules/bootstrap/scss/functions";

$primary: #0f6e5a;
$border-radius: .5rem;

@import "../node_modules/bootstrap/scss/bootstrap";

Compile it into the static folder with npx sass scss/custom.scss static/css/bootstrap-custom.css. Then point the stylesheet tag in base.html at {% static 'css/bootstrap-custom.css' %}.

Order matters. Bootstrap declares its variables with the !default flag, so the first assignment wins, and a value set after the variables import changes nothing.

You can run Dart Sass as a build step, using the CLI above and either committing the output or building it during deploy. django-sass-processor is the other option, since its sass_src tag compiles on render, though its documented install line pulls in libsass.

The Sass team declared LibSass end-of-life in October 2025, tells legacy projects to migrate to Dart Sass, and notes that LibSass supports only @import, not the module system. For a new Django project, a Dart Sass build step is the safer default.

Bootstrap’s 5.3 docs still show @import, while Dart Sass has deprecated that rule (since 1.80.0) and plans to remove it in version 3.0.0. Pin the sass package below 3.0 until Bootstrap’s source drops it, and expect deprecation warnings in the meantime.

Overriding with a custom CSS file

For a handful of changes, skip the build step.

A plain CSS file is enough when you’re changing a few colors or fonts, maybe some spacing, and it all fits in a short stylesheet. Sass is for rebuilding the palette, editing breakpoints, or importing only the components you use to shrink the output.

Create static/css/custom.css, link it after Bootstrap’s stylesheet in base.html, and let the later rule win at equal specificity. This guide to overriding Bootstrap CSS covers selector specificity in more depth.

Bootstrap 5.3 exposes CSS custom properties with a --bs- prefix, but components define local ones too, such as --bs-btn-bg on buttons. Changing only a root variable does not recolor every component.

Serving Bootstrap files in production with collectstatic and WhiteNoise

Run collectstatic whenever static files change, then serve the STATIC_ROOT folder. That is the whole outline in the Django deployment docs, and the open question is who serves the folder.

The docs leave that open, so any of these works.

  • The same server, through your web server configuration
  • A dedicated static server, synced with rsync
  • A cloud storage service or CDN

WhiteNoise is another route, suited to small deployments. Django serves its own files, so there’s no separate web server rule to maintain.

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",

# ...

]

STATIC_ROOT = BASE_DIR / "staticfiles"

STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
python manage.py collectstatic --noinput

Placement follows the WhiteNoise 6.12.0 docs, which say directly after SecurityMiddleware and before all other middleware. LearnDjango’s tutorial puts it after SessionMiddleware, so follow the official order.

Watch the setting name too. Django 4.2 introduced STORAGES, and Django 5.1 (August 2024) removed STATICFILES_STORAGE and DEFAULT_FILE_STORAGE. Appliku’s WhiteNoise guide still shows STATICFILES_STORAGE.

On Django 5.1 or newer the old setting is silently ignored, so collectstatic runs but filenames never get hashes. Django ticket 35674 records the same silent fallback for DEFAULT_FILE_STORAGE, and the 5.1 removal covers both settings.

CompressedManifestStaticFilesStorage combines compression with the caching behavior of Django’s manifest storage. Each file gets a hash before its extension, so a Bootstrap upgrade produces a new filename and browsers stop serving the cached copy.

With manifest storage on and DEBUG off, a static tag that points at a file missing from staticfiles.json raises ValueError: Missing staticfiles manifest entry and returns a 500 page.

Sometimes the file was simply never collected, because collectstatic was skipped or ran before the file existed. A leading slash does it too, since {% static '/bootstrap/css/bootstrap.min.css' %} misses the manifest, which stores relative paths (Django ticket 32131).

The least obvious cause is an unreadable manifest. In Django releases before the fix for ticket 30599 (closed in 2019), a permissions error on staticfiles.json was swallowed and surfaced as this same ValueError. Current releases raise the PermissionError itself.

If the error page itself needs a static file, it fails the same way, so the server log is the place to look. Django ticket 33324 describes how hard this is to debug (it was closed for lack of a reproduction).

Why is Bootstrap not loading in Django?

Usually Django can’t resolve a static path, a deployment step got skipped, or the markup and scripts were written for a different Bootstrap version. The browser console tells you which.

Open the console first. A failed to load resource message with a 404 status shows the exact URL Django could not find.

Then ask Django where it thinks the file lives. python manage.py findstatic bootstrap/css/bootstrap.min.css prints the resolved path, or reports that nothing matched.

SymptomLikely causeFix
Unstyled page, 404 on bootstrap.min.cssPath in the static tag does not match the folder under STATICFILES_DIRSMatch the path, confirm with findstatic
Invalid block tag: ‘static’No load static line in that templateAdd it to every template that uses the tag, child templates included
Styled locally, bare after deployFiles not collected, or nothing serves STATIC_ROOTRun collectstatic, add WhiteNoise or a server rule
500 page, Missing staticfiles manifest entryFile absent from staticfiles.jsonRe-run collectstatic, drop the leading slash, check permissions
Modals and dropdowns ignore clicksVersion 4 data-toggle attributes in a version 5 projectRename to data-bs-toggle and data-bs-target
Template error: not a registered tag libraryLoad tag does not match the installed packageUse the tag that fits the package name
CDN files blocked, console says Refused to loadA Content-Security-Policy header excludes the CDN hostAllow cdn.jsdelivr.net in the script and style rules

Check the network tab before the code. A 200 for both Bootstrap files rules out settings, paths and collectstatic in one look.

Bootstrap in Django FAQ

Does Django come with Bootstrap?

No. Django ships its own admin stylesheets but no Bootstrap files.

You add Bootstrap yourself through a CDN link, local static files or the django-bootstrap5 package.

Do I need collectstatic if I load Bootstrap from a CDN?

Not for Bootstrap itself, because the browser fetches those files from jsDelivr.

Your own CSS, JavaScript and images still need collectstatic on deploy, so keep STATIC_ROOT configured.

How do I add Bootstrap Icons to a Django project?

Install the package with npm install bootstrap-icons, copy its font folder into static/, and link bootstrap-icons.css in base.html. Keep the fonts folder next to the CSS file, since the stylesheet loads fonts by relative path.

Then use <i class="bi bi-alarm"></i> wherever an icon belongs.

Can I use Bootstrap in the Django admin?

Not directly. The admin loads its own stylesheets, and the django-bootstrap-admin package on PyPI describes itself as adding Twitter Bootstrap to it.

Its last release dates from November 2013, so treat it as unmaintained and test it carefully against your Django version before relying on it.

Keeping Bootstrap in Django Up to Date

Pick the Django release first and pin the Bootstrap version second. Package support follows Django’s schedule, not Bootstrap’s.

The django-bootstrap-v5 package caps Django below 5.0, and the Django project ended extended support for 4.2 LTS on 7 April 2026. Every Django release that package accepts is now end-of-life.

  • Move to Django 5.2 LTS (security fixes until April 2028) or a newer release such as 6.1
  • Pin the Bootstrap version, either in the CDN URL or in the static folder
  • Retest forms and dropdowns after each minor release

Pinning trades automatic CDN fixes for stability, so upgrades become scheduled work.

As of 11 October 2026, this advice changes if Django 6.2 LTS (April 2027) or a Bootstrap 5.4 release arrives. With versions settled, Bootstrap components such as cards and modals are the next build.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)