How to Add Dark Mode to Any Bootstrap 5 HTML Template

  • Canvas Team
  • 14 min read
How to Add Dark Mode to Any Bootstrap 5 HTML Template
14 min read
Share:

How to Add Dark Mode to Any Bootstrap 5 HTML Template

How to Add Dark Mode to Any Bootstrap 5 HTML Template

Dark mode has moved from a niche preference to a genuine user expectation. In 2026, if your Bootstrap 5 HTML template does not support it, you are leaving a significant slice of your audience squinting at a blinding white screen — especially late-night users on OLED displays. The good news is that Bootstrap 5.3 introduced first-class dark mode support, and even if you are running an older Bootstrap 5 build, implementing a solid dark mode toggle requires surprisingly little code.

In this guide you will learn exactly how to add dark mode to any Bootstrap 5 HTML template — from the native data-bs-theme attribute approach, through CSS custom properties, to a JavaScript toggle that persists user preference across sessions. Every approach includes real, copy-pasteable code. Whether you are customising a premium template like Canvas Template or building something from scratch, this walkthrough has you covered.

Key Takeaways

Key Takeaways

  • Bootstrap 5.3+ includes a native data-bs-theme attribute that enables dark mode with zero extra CSS for core components.
  • CSS custom properties (variables) are the cleanest way to extend dark mode to your own custom components beyond Bootstrap’s defaults.
  • A JavaScript toggle that writes to localStorage ensures users do not have to re-select their preference on every page load.
  • The prefers-color-scheme media query lets you respect the OS-level preference automatically as a sensible default.
  • Accessibility is non-negotiable — always verify contrast ratios in both light and dark modes against WCAG 2.1 AA standards.
  • SCSS variable overrides give you granular control over dark mode colours when using a build pipeline.

Understanding Bootstrap 5.3’s Dark Mode Architecture

Before writing a single line of code it is worth understanding how Bootstrap 5.3 implements dark mode under the hood. The framework ships two colour mode styles — light and dark — controlled by a single HTML attribute: data-bs-theme. Apply it to the <html> element to switch the entire page, or scope it to any individual component.

Internally, Bootstrap maps its component colours to a set of CSS custom properties (--bs-body-bg, --bs-body-color, --bs-card-bg, and so on). When data-bs-theme="dark" is present, Bootstrap redefines those custom properties with dark-appropriate values. Every component — cards, navbars, modals, dropdowns, badges — responds automatically.

If you are on Bootstrap 5.0–5.2, this native mechanism does not exist, and you will need to rely entirely on a custom CSS class approach. We will cover both paths below.

<!-- Bootstrap 5.3+: whole-page dark mode -->
<html lang="en" data-bs-theme="dark">

<!-- Bootstrap 5.3+: scoped dark mode on a single card -->
<div class="card" data-bs-theme="dark">
  <div class="card-body">
    <h5 class="card-title">Dark Card</h5>
    <p class="card-text">Only this card is dark.</p>
  </div>
</div>

The scoped approach is particularly powerful when you want individual sections — a hero, a sidebar, a footer — to sit in dark mode while the rest of the page remains light. This is a pattern used extensively in Canvas Template’s multi-section layouts.

Using CSS Custom Properties to Extend Dark Mode to Custom Components

Bootstrap’s built-in dark mode covers its own components perfectly, but your template almost certainly has custom UI elements — hero sections, pricing tables, timeline blocks, testimonial cards — that Bootstrap knows nothing about. This is where you extend the system using the same CSS custom property pattern Bootstrap itself uses.

The recommended pattern is to define your custom properties under :root for light mode and then override them inside a [data-bs-theme="dark"] selector block.

/* light mode defaults — defined on :root */
:root {
  --custom-hero-bg: #f8f9fa;
  --custom-hero-color: #212529;
  --custom-card-border: #dee2e6;
  --custom-highlight: #0d6efd;
  --custom-muted-bg: #e9ecef;
}

/* dark mode overrides */
[data-bs-theme="dark"] {
  --custom-hero-bg: #0f1117;
  --custom-hero-color: #e8eaf0;
  --custom-card-border: #2c2f36;
  --custom-highlight: #6ea8fe;
  --custom-muted-bg: #1a1d24;
}

/* usage in component styles */
.hero-section {
  background-color: var(--custom-hero-bg);
  color: var(--custom-hero-color);
}

.feature-card {
  border: 1px solid var(--custom-card-border);
  background-color: var(--custom-muted-bg);
}

Because these properties cascade, any component that consumes them will automatically flip when the theme attribute changes — no JavaScript required for the visual update, only for toggling the attribute itself.

If your project uses SCSS, this integrates beautifully with Bootstrap’s variable system. Our guide on how to use SCSS variables to theme a Bootstrap 5 site goes deeper on the build-pipeline side of this, including how to override $body-bg, $body-color, and component-level SCSS maps before compilation.

Building the JavaScript Toggle With localStorage Persistence

A dark mode toggle is only genuinely useful if it remembers the user’s choice. Here is a complete, production-ready implementation that reads and writes to localStorage, respects the OS preference as a starting default, and updates a toggle button’s accessible label.

<!-- Toggle button — place in your navbar -->
<button
  id="theme-toggle"
  type="button"
  class="btn btn-sm btn-outline-secondary"
  aria-label="Switch to dark mode"
>
  <svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
    <path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13z"/>
  </svg>
  <svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" class="d-none">
    <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
  </svg>
</button>
// theme-toggle.js
(function () {
  const htmlEl = document.documentElement;
  const toggleBtn = document.getElementById('theme-toggle');
  const iconSun = document.getElementById('icon-sun');
  const iconMoon = document.getElementById('icon-moon');

  // Determine initial theme:
  // 1. Check localStorage first
  // 2. Fall back to OS preference
  // 3. Default to light
  function getPreferredTheme() {
    const stored = localStorage.getItem('bs-theme');
    if (stored) return stored;
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light';
  }

  function applyTheme(theme) {
    htmlEl.setAttribute('data-bs-theme', theme);
    localStorage.setItem('bs-theme', theme);

    if (theme === 'dark') {
      iconSun.classList.remove('d-none');
      iconMoon.classList.add('d-none');
      toggleBtn.setAttribute('aria-label', 'Switch to light mode');
    } else {
      iconSun.classList.add('d-none');
      iconMoon.classList.remove('d-none');
      toggleBtn.setAttribute('aria-label', 'Switch to dark mode');
    }
  }

  // Apply on page load — run before DOMContentLoaded to avoid flash
  applyTheme(getPreferredTheme());

  toggleBtn.addEventListener('click', function () {
    const current = htmlEl.getAttribute('data-bs-theme');
    applyTheme(current === 'dark' ? 'light' : 'dark');
  });

  // Sync if OS preference changes while page is open
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
    if (!localStorage.getItem('bs-theme')) {
      applyTheme(e.matches ? 'dark' : 'light');
    }
  });
})();

A critical detail: call applyTheme(getPreferredTheme()) as early as possible — ideally in a <script> tag placed immediately after the opening <body> tag, before any visible content renders. This eliminates the “flash of wrong theme” (FOWT) that plagues many dark mode implementations.

Dark Mode for Bootstrap 5.0–5.2 (The Class-Based Approach)

If you are using Bootstrap 5.0, 5.1, or 5.2, the data-bs-theme attribute is not available. The established approach here is a toggling CSS class — typically .dark-mode — on the <body> or <html> element, combined with a full set of overriding styles.

/* styles for Bootstrap 5.0-5.2 dark mode */
body.dark-mode {
  background-color: #0f1117;
  color: #e8eaf0;
}

body.dark-mode .navbar {
  background-color: #1a1d24 !important;
}

body.dark-mode .card {
  background-color: #1e2128;
  border-color: #2c2f36;
  color: #e8eaf0;
}

body.dark-mode .card-header,
body.dark-mode .card-footer {
  background-color: #252830;
  border-color: #2c2f36;
}

body.dark-mode .form-control,
body.dark-mode .form-select {
  background-color: #1a1d24;
  border-color: #3a3d46;
  color: #e8eaf0;
}

body.dark-mode .table {
  --bs-table-bg: #1e2128;
  --bs-table-striped-bg: #252830;
  --bs-table-hover-bg: #2a2d36;
  color: #e8eaf0;
  border-color: #2c2f36;
}
// JavaScript for Bootstrap 5.0-5.2
const body = document.body;
const toggleBtn = document.getElementById('theme-toggle');

const saved = localStorage.getItem('dark-mode');
if (saved === 'true' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
  body.classList.add('dark-mode');
}

toggleBtn.addEventListener('click', function () {
  body.classList.toggle('dark-mode');
  localStorage.setItem('dark-mode', body.classList.contains('dark-mode'));
});

This approach is more verbose but works reliably. The main downside compared to the 5.3 approach is that you need to maintain a comprehensive override list as you add new components. For large templates with many custom components, this can become significant CSS maintenance overhead.

Comparing Dark Mode Approaches: Which Should You Use?

Approach Bootstrap Version CSS Effort JS Effort Component Coverage Best For
data-bs-theme attribute 5.3+ Low (only custom components) Low Excellent (automatic) New projects, Canvas Template users
CSS custom properties extension 5.3+ Medium None extra Full (custom + Bootstrap) Extending any 5.3+ template
Body class toggle 5.0–5.2 High (full overrides) Low Manual — whatever you write Legacy Bootstrap 5 projects
Separate dark stylesheet Any Very High Medium Full control Large teams with design system
SCSS-compiled dark theme Any Medium (SCSS maps) Low Full (compiled in) Build-pipeline projects

For most developers working with a modern Bootstrap 5.3 template in 2026, the combination of data-bs-theme on the <html> element plus CSS custom properties for custom components is the clear winner — it is the least code for the most coverage.

Accessibility and Contrast in Dark Mode

Adding dark mode is not simply a visual swap — it is an accessibility feature in its own right, and it can just as easily introduce accessibility problems if not tested rigorously. WCAG 2.1 AA requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text, in both modes.

Common dark mode contrast failures include:

  • Muted text colours (text-muted, .text-secondary) that already barely pass in light mode dropping below threshold in dark mode
  • Placeholder text in form inputs becoming invisible
  • Disabled state colours that blend into dark backgrounds
  • Icon colours not updating alongside text colours
/* Fix common contrast failures in dark mode */
[data-bs-theme="dark"] .text-muted {
  color: #9aa0ac !important; /* passes 4.6:1 on #0f1117 bg */
}

[data-bs-theme="dark"] ::placeholder {
  color: #6c757d;
  opacity: 1;
}

[data-bs-theme="dark"] .form-control:disabled {
  background-color: #2a2d36;
  color: #6c757d;
}

Always run both your light and dark mode versions through a contrast checker such as the WebAIM Contrast Checker or the browser’s built-in accessibility panel. Our detailed post on making a Bootstrap 5 website accessible to WCAG 2.1 AA covers systematic contrast auditing, focus indicators, and ARIA patterns that you should apply in parallel with your dark mode work.

Also remember to update ARIA labels on your toggle button dynamically, as shown in the JavaScript snippet above. Screen reader users need to know what action the button will perform, not what mode is currently active — so “Switch to light mode” is the correct label when dark mode is on.

Handling Images, SVGs, and Media in Dark Mode

One area developers consistently overlook is imagery. A bright white logo or illustration can look jarring on a dark background, and certain photographs lose their intended mood when surrounded by dark chrome.

Several practical techniques help here:

/* Reduce brightness of photographs in dark mode */
[data-bs-theme="dark"] img:not([data-no-dim]) {
  filter: brightness(0.85) contrast(1.05);
  transition: filter 0.3s ease;
}

/* Invert simple black-on-white SVG logos */
[data-bs-theme="dark"] .logo-img {
  filter: invert(1) hue-rotate(180deg);
}

/* Use the CSS picture element trick for art-directed dark images */
<!-- Art-directed dark mode image using <picture> -->
<picture>
  <source
    srcset="/images/hero-dark.webp"
    media="(prefers-color-scheme: dark)"
  />
  <img src="/images/hero-light.webp" alt="Hero illustration" class="img-fluid" />
</picture>

The <picture> element approach is ideal for hero illustrations and brand assets where you have specifically designed dark variants. For general photography, the filter: brightness() approach is a decent automatic solution, though it should be applied selectively rather than globally.

For SVG icons inline in the DOM, they naturally inherit currentColor if built correctly, which means they will respond to your dark mode text colour changes without any extra work. This is one of many reasons to prefer inline SVG over icon fonts or <img>-embedded SVGs where possible.

Performance Considerations and Dark Mode Best Practices

Dark mode, if implemented carelessly, can introduce layout shift, flash of wrong theme, or unnecessary render-blocking. Here are the practices that keep things fast and smooth.

Avoid FOWT with an inline script: Place the theme-detection script in a <script> tag in the <head> — not deferred, not async. Yes, this is a rare case where a synchronous script in the head is the correct decision. It reads from localStorage and sets the attribute before the browser paints anything.

<head>
  <meta charset="UTF-8" />
  <!-- Inline FOWT prevention script — keep synchronous -->
  <script>
    (function() {
      var theme = localStorage.getItem('bs-theme') ||
        (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
      document.documentElement.setAttribute('data-bs-theme', theme);
    })();
  </script>
  <link rel="stylesheet" href="bootstrap.min.css" />
  <link rel="stylesheet" href="style.css" />
</head>

Use CSS transitions sparingly: Adding a global transition: background-color 0.3s, color 0.3s to the body sounds nice but it triggers repaints on every property of every element simultaneously. Instead, apply transitions only to elements where the animation adds genuine value — the toggle button icon, a hero section background, navigation bars.

Lazy-load dark-mode-specific assets: If your dark mode uses entirely different hero images, consider loading them dynamically via JavaScript only when dark mode is active rather than bundling both into every page load. This ties into the broader performance conversation — our post on page speed optimisation for Bootstrap 5 HTML templates covers asset loading strategies in detail.

Test in both modes during development: It sounds obvious but many bugs only appear in dark mode — box shadows that disappear, borders that vanish into backgrounds, focus rings that become invisible. Add dark mode testing to your standard browser QA checklist.

If you want to skip manual implementation entirely and work in a visual builder environment, CanvasBuilder — the AI website builder built on top of Canvas Template — handles dark mode configuration through a simple UI toggle, generating all the correct attributes and custom property overrides for you. It is a great option if you want the flexibility of Bootstrap 5 without hand-coding every theme variable.

For a deeper look at theming patterns in Canvas Template specifically, the post on Bootstrap 5 card component variants shows how card elements respond to theme attributes, which is helpful when you are building content-heavy dark mode layouts.


Frequently Asked Questions

Does Bootstrap 5 support dark mode natively?

Yes, but only from version 5.3 onwards. Bootstrap 5.3 introduced the data-bs-theme attribute which switches all built-in components between light and dark colour palettes using CSS custom properties. If you are on Bootstrap 5.0–5.2, you need to implement dark mode manually via a toggling CSS class and override styles.

How do I prevent the flash of wrong theme (FOWT) in Bootstrap 5?

Place a small inline (synchronous) <script> tag in the <head> element — before your stylesheets if possible, but at minimum before any visible content. This script reads from localStorage and sets data-bs-theme on the <html> element immediately. Because it runs synchronously, the browser sets the attribute before it renders any content, eliminating the flash.

Can I apply dark mode to only part of a Bootstrap 5 page?

Yes. The data-bs-theme attribute can be applied to any HTML element, not just the root element. You could have a dark navigation bar (<nav data-bs-theme="dark">), a light main content area, and a dark footer all on the same page. This scoped approach is one of the most powerful features of Bootstrap 5.3’s implementation.

How do I make dark mode work with custom components that Bootstrap doesn’t know about?

Use CSS custom properties. Define your custom component colours as CSS variables under :root for light mode, then redefine them inside a [data-bs-theme="dark"] selector block. Your component styles consume those variables, so they automatically update when the theme attribute changes. No additional JavaScript is needed for the visual update — only for toggling the attribute itself.

Should I use prefers-color-scheme or a manual toggle for dark mode?

Both — in combination. Use prefers-color-scheme as the sensible default when a user visits your site for the first time with no stored preference. Then give them a manual toggle to override it, persisting their choice in localStorage. Also listen for changes to the OS-level preference and update automatically if the user has not manually overridden it. This covers all scenarios and respects user autonomy.


Ready to Build With Dark Mode From Day One?

Stop retrofitting dark mode onto templates that were never designed for it. Canvas Template is a premium Bootstrap 5.3 HTML template built with CSS custom properties throughout, making dark mode implementation clean, fast, and maintainable. Every component is structured to respond to data-bs-theme out of the box.

Prefer a visual approach? CanvasBuilder — the AI website builder — lets you configure dark mode, colour palettes, and component themes without touching a line of CSS.

Skip the setup — build it free

Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.

Share:
Canvas Team
Canvas Team

Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.

More from the Canvas Blog