Lesson 12 of 12

Customize Bootstrap and Dark Mode

A site that uses Bootstrap’s default blue buttons and fonts looks like thousands of others. In this last lesson you will learn how to make Bootstrap your own: change colours, fonts and corner radius with CSS variables, go further with Sass, and switch on Bootstrap dark mode (and back to light) with a single attribute.

Three ways to customise

ApproachGood forNeeds a build step?
Utility classes and your own CSSSmall, local changesNo
Overriding CSS variablesBrand colours, fonts, radius and component tweaks on a CDN setupNo
Sass variablesA full theme: every colour shade, spacing scale, breakpoints, removing unused partsYes (npm and a Sass compiler)

Always load your own stylesheet after Bootstrap’s. When two rules have the same specificity, the later one wins, so your overrides take effect.

Customising with CSS variables

Bootstrap 5.3 exposes many design values as CSS custom properties (variables) on :root, all starting with --bs-. For example --bs-body-font-family, --bs-body-color, --bs-border-radius and --bs-link-color-rgb (links read the RGB version). Change one, and every component that reads it updates. The CSS Variables lesson explains how custom properties work.

Colours need a little care. Utilities such as text-primary and bg-primary read the --bs-primary-rgb variable (three numbers, so Bootstrap can add opacity), not --bs-primary. Buttons are different again: in the compiled CSS, .btn-primary sets its own local variables like --bs-btn-bg to fixed colour values. So changing --bs-primary alone does not recolour your buttons. You override the button variables on .btn-primary directly, as in this example:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brand colours with CSS variables</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<style>
/* Global design values */
:root {
  --bs-primary: #0f766e;
  --bs-primary-rgb: 15, 118, 110;
  --bs-link-color: #0f766e;
  --bs-link-color-rgb: 15, 118, 110;
  --bs-link-hover-color: #115e59;
  --bs-link-hover-color-rgb: 17, 94, 89;
  --bs-body-font-family: Georgia, "Times New Roman", serif;
  --bs-border-radius: 0.75rem;
}

/* Buttons keep their own local variables */
.btn-primary {
  --bs-btn-bg: #0f766e;
  --bs-btn-border-color: #0f766e;
  --bs-btn-hover-bg: #115e59;
  --bs-btn-hover-border-color: #115e59;
  --bs-btn-active-bg: #134e4a;
  --bs-btn-active-border-color: #134e4a;
  --bs-btn-focus-shadow-rgb: 15, 118, 110;
}
.btn-outline-primary {
  --bs-btn-color: #0f766e;
  --bs-btn-border-color: #0f766e;
  --bs-btn-hover-bg: #0f766e;
  --bs-btn-hover-border-color: #0f766e;
  --bs-btn-active-bg: #115e59;
  --bs-btn-active-border-color: #115e59;
  --bs-btn-focus-shadow-rgb: 15, 118, 110;
}
</style>
</head>
<body>
<div class="container py-4">
  <h1 class="h3">Peacock Tea House</h1>
  <p class="text-primary fw-semibold">Fresh masala chai and snacks in Mylapore.</p>
  <p>Read our <a href="#">opening hours</a> or book a table.</p>
  <div class="d-flex flex-wrap gap-2 mb-4">
    <button type="button" class="btn btn-primary">Book a table</button>
    <button type="button" class="btn btn-outline-primary">See menu</button>
  </div>
  <div class="card" style="max-width: 22rem;">
    <div class="card-body">
      <h2 class="card-title h5">Today's special</h2>
      <p class="card-text">Ginger chai with hot bajji.</p>
      <span class="badge text-bg-primary">Chef's pick</span>
    </div>
  </div>
</div>
</body>
</html>

The font, the corner radius on the buttons and card, the link colour, the primary text colour and the badge all change from a few lines. This approach works with the CDN, needs no tools, and is a good fit for small sites. Its limit is that many lighter and darker shades (such as bg-primary-subtle and the active state colours) are separate variables, so a full re-brand means overriding quite a few of them.

Customising with Sass

For a full theme, use Bootstrap’s Sass source files. You install Bootstrap with npm, write your own .scss file that sets variables before importing Bootstrap, and compile it to CSS. Bootstrap then generates every shade, utility and component from your values, so $primary really does update buttons, alerts, badges and subtle backgrounds together.

In a project folder with Node.js installed, run npm install [email protected] and npm install --save-dev sass. Create the file below, then compile it with npx sass scss/custom.scss css/custom.css and link css/custom.css in your pages instead of the CDN stylesheet.

// 1. Bootstrap functions first
@import "../node_modules/bootstrap/scss/functions";

// 2. Your variable overrides (no !default needed)
$primary: #0f766e;
$font-family-sans-serif: "Noto Sans", system-ui, sans-serif;
$border-radius: .75rem;
$enable-shadows: true;
$enable-negative-margins: true;

// 3. The rest of Bootstrap
@import "../node_modules/bootstrap/scss/bootstrap";

Tip: Recent versions of Dart Sass print deprecation warnings about @import when compiling Bootstrap 5.3. The Bootstrap team says these warnings do not stop compilation and can be ignored for now. Bootstrap’s docs also show how to import only the parts you use, which makes the final CSS smaller.

Bootstrap dark mode and colour modes

Since version 5.3, Bootstrap supports colour modes. To turn on Bootstrap dark mode, add data-bs-theme="dark" to the <html> element and the whole page switches to dark colours. Bootstrap does this by redefining its CSS variables under the selector [data-bs-theme=dark]: the body background becomes dark grey, text becomes light, and borders, form fields, cards and dropdowns follow.

This only works for classes that read those variables. bg-body, bg-body-tertiary, text-body-emphasis and text-body-secondary adapt to the mode. Fixed colours such as bg-white, bg-light and text-dark do not, so a bg-light panel stays bright in dark mode. When you plan to support Bootstrap dark mode, choose the adaptive classes from the start.

<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A light, dark and auto theme switcher</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container py-4">
  <div class="btn-group mb-3" role="group" aria-label="Colour mode">
    <button type="button" class="btn btn-outline-secondary" data-mode="light">Light</button>
    <button type="button" class="btn btn-outline-secondary" data-mode="dark">Dark</button>
    <button type="button" class="btn btn-outline-secondary" data-mode="auto">Auto</button>
  </div>
  <p id="modeInfo" role="status"></p>

  <div class="card mb-3">
    <div class="card-body">
      <h2 class="card-title h5">Monsoon reading list</h2>
      <p class="card-text text-body-secondary">This card uses adaptive colours, so it follows the mode.</p>
      <label for="note" class="form-label">Add a note</label>
      <input id="note" class="form-control" placeholder="Form fields adapt too">
    </div>
  </div>
  <div class="p-3 mb-3 bg-body-tertiary border rounded">bg-body-tertiary adapts to the mode.</div>
  <div class="p-3 bg-light text-dark border rounded">bg-light text-dark stays light in both modes.</div>
</div>
<script>
const media = window.matchMedia('(prefers-color-scheme: dark)');
let chosen = 'auto';

function applyMode() {
  const mode = chosen === 'auto' ? (media.matches ? 'dark' : 'light') : chosen;
  document.documentElement.setAttribute('data-bs-theme', mode);
  document.querySelectorAll('[data-mode]').forEach((btn) => {
    const isOn = btn.dataset.mode === chosen;
    btn.classList.toggle('active', isOn);
    btn.setAttribute('aria-pressed', String(isOn));
  });
  document.getElementById('modeInfo').textContent =
    'Chosen: ' + chosen + '. Showing: ' + mode + ' mode.';
}

document.querySelectorAll('[data-mode]').forEach((btn) => {
  btn.addEventListener('click', () => {
    chosen = btn.dataset.mode;
    applyMode();
  });
});
media.addEventListener('change', applyMode);
applyMode();
</script>
</body>
</html>

“Auto” follows the operating system setting through the prefers-color-scheme media query, and the change listener updates the page if the user switches their system theme while it is open. On a real site you would also remember the visitor’s choice. The snippet below saves it in localStorage. Run it in the <head>, before the page is drawn, so the page does not flash light before turning dark.

const saved = localStorage.getItem('theme');          // 'light', 'dark' or null
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = saved || (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-bs-theme', theme);

// When the user picks a mode:
function setTheme(mode) {
  localStorage.setItem('theme', mode);
  document.documentElement.setAttribute('data-bs-theme', mode);
}

Per-component modes and custom dark colours

data-bs-theme also works on any single element. A dark footer or navbar on a light page, or a light card inside a dark section, is just data-bs-theme="dark" or "light" on that element. You can tune the dark palette too: redefine variables inside [data-bs-theme=dark] in your own CSS.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mixing modes on one page</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<style>
[data-bs-theme=dark] {
  --bs-body-bg: #0b1d33;
  --bs-body-bg-rgb: 11, 29, 51;
}
</style>
</head>
<body>
<div class="container py-4">
  <p>This page is in light mode.</p>
  <section class="p-4 rounded-3 bg-body text-body border" data-bs-theme="dark">
    <h2 class="h5">A dark section</h2>
    <p class="text-body-secondary">Everything inside uses the dark palette, with our own deep-blue background.</p>
    <button type="button" class="btn btn-outline-light btn-sm">Learn more</button>
    <div class="card mt-3" data-bs-theme="light" style="max-width: 18rem;">
      <div class="card-body">
        <p class="card-text mb-0">A light card inside the dark section.</p>
      </div>
    </div>
  </section>
</div>
</body>
</html>

Watch out: Do not simply invert colours for dark mode. Pure black backgrounds with pure white text cause glare, saturated brand colours can vibrate on dark grey, and shadows barely show. Test contrast in both modes. The post Dark Mode Design and CSS covers these design decisions, and CSS Variables as Design Tokens shows how to organise a theme’s values.

Where to go next

You now know the main parts of Bootstrap 5: the grid, utilities, the most-used components, forms, and how to theme it. The best next step is to build a small, complete page, such as a portfolio or a shop landing page, using only what you have learned. When you are ready to add more interactivity, continue with the JavaScript track, or compare approaches in Bootstrap vs Tailwind CSS.

Further reading

Key takeaways

  • Load your CSS after Bootstrap; override --bs-* variables for fonts, radius, links and colours.
  • Utilities read --bs-primary-rgb, and buttons use local --bs-btn-* variables, so override those as well.
  • For a full theme, set Sass variables such as $primary before importing Bootstrap and compile.
  • data-bs-theme="dark" on <html> switches the page to dark mode; it also works on single elements.
  • Use adaptive classes (bg-body-tertiary, text-body-secondary) and test contrast in both modes.

Quick quiz

Check what you learned – pick an answer.

1. You set --bs-primary: #0f766e on :root, but btn-primary stays blue. Why?

2. Which class keeps a light background even in dark mode?

3. How do you make only the footer dark on an otherwise light page?

Practice exercise

Create a page with a "Dark mode" switch (a Bootstrap form-switch). When it is switched on, set data-bs-theme="dark" on the <html> element; when off, set it back to light. Show the current mode in a line of text, and include a card with adaptive colours so the change is visible. Also change the corner radius to 1rem with a CSS variable.

Show a hint

Listen for the change event on the checkbox and use document.documentElement.setAttribute().

Show the solution
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dark mode switch</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<style>
:root {
  --bs-border-radius: 1rem;
}
</style>
</head>
<body>
<div class="container py-4">
  <div class="form-check form-switch mb-3">
    <input class="form-check-input" type="checkbox" role="switch" id="darkSwitch">
    <label class="form-check-label" for="darkSwitch">Dark mode</label>
  </div>
  <p id="modeText" role="status">Current mode: light</p>
  <div class="card" style="max-width: 22rem;">
    <div class="card-body">
      <h2 class="card-title h5">Night trains from Chennai</h2>
      <p class="card-text text-body-secondary">Sleep on the train and wake up in Madurai, Coimbatore or Kanniyakumari.</p>
    </div>
  </div>
</div>
<script>
const darkSwitch = document.getElementById('darkSwitch');
darkSwitch.addEventListener('change', () => {
  const mode = darkSwitch.checked ? 'dark' : 'light';
  document.documentElement.setAttribute('data-bs-theme', mode);
  document.getElementById('modeText').textContent = 'Current mode: ' + mode;
});
</script>
</body>
</html>

Progress is saved in this browser only.