Colours & Dark Mode

Bootstrap · Lesson 6 of 28 7 min read Updated

Colour tells users what matters: green for success, red for errors, blue for the main action. Bootstrap gives you a small, consistent palette and classes to apply it to text, backgrounds and borders. Since version 5.3 it also has built-in colour modes, so a whole page or a single component can switch to a dark theme with one attribute.

In this lesson you will use the theme colours, the newer subtle and emphasis shades, and build a working dark mode toggle.

Theme colours

Bootstrap names its colours by purpose, not by appearance. That way, if a company's brand colour is orange, you change primary once and every primary button, link and badge follows.

NameUsual meaning
primaryThe main action or brand colour
secondaryLess important actions
successSomething worked: payment received, form saved
dangerErrors and destructive actions such as Delete
warningNeeds attention: low stock, unsaved changes
infoNeutral information
light and darkLight grey and near-black surfaces

Each name works with many prefixes: text-success, bg-danger, border-warning, btn-primary, alert-info and so on.

Text and background utilities

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Colours</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container py-3">
    <h1 class="h4">Order #4521, Lucknow</h1>
    <p class="text-success fw-semibold">Payment of ₹1,299 received.</p>
    <p class="text-danger">One item is out of stock.</p>
    <p class="text-body-secondary">Ordered on 28 October, 6:40 pm</p>

    <div class="p-3 mb-2 text-bg-primary rounded">text-bg-primary: background with a readable text colour</div>
    <div class="p-3 mb-2 text-bg-warning rounded">text-bg-warning: dark text chosen automatically</div>
    <div class="p-3 mb-2 bg-success-subtle text-success-emphasis border border-success-subtle rounded">
      Subtle background, emphasis text and subtle border: a calm success message
    </div>
    <div class="p-3 mb-2 bg-danger bg-opacity-25 rounded">bg-danger at 25% opacity</div>
    <div class="p-3 bg-body-tertiary rounded">bg-body-tertiary: a soft panel colour</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

The main families of colour classes:

  • text-{color} and bg-{color} set only the text or only the background. With bg-primary you must also pick a text colour yourself.
  • text-bg-{color} sets the background and a contrasting text colour in one class. It is the safest choice for coloured boxes and badges.
  • bg-{color}-subtle, text-{color}-emphasis and border-{color}-subtle (new in 5.3) give pale backgrounds and darker text that look softer than the full colour. They also adapt automatically in dark mode.
  • text-body, text-body-secondary, text-body-tertiary and text-body-emphasis are the page's own text colours, from normal to faint to strongest.
  • bg-body, bg-body-secondary and bg-body-tertiary are the page's own surface colours, good for panels and sidebars.
  • bg-opacity-{10|25|50|75|100} and text-opacity-{25|50|75|100} fade a colour.

There are also link colour classes such as link-primary, link-body-emphasis and link-underline-opacity-25 for styling links.

Colour modes and data-bs-theme

Bootstrap 5.3 stores its colours in CSS variables (custom properties such as --bs-body-bg and --bs-body-color; see CSS Custom Properties). A colour mode is a different set of values for those variables. Bootstrap ships two modes, light (the default) and dark, and you choose one with the data-bs-theme attribute:

HTML
<!-- Whole page in dark mode -->
<html lang="en" data-bs-theme="dark">

<!-- Only one part of the page in dark mode -->
<div class="card" data-bs-theme="dark"> ... </div>

<!-- A light island inside a dark page -->
<div data-bs-theme="light"> ... </div>

The attribute works on any element, and everything inside it follows. Body text, backgrounds, borders, form fields, cards, dropdowns, modals and tables all switch together because they read the same variables. You do not write any dark mode CSS yourself.

A light/dark toggle with JavaScript

Users like a switch that remembers their choice. The plan is: on page load, use the saved choice if there is one, otherwise follow the operating system setting (prefers-color-scheme); when the button is clicked, flip the mode and save it in localStorage.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Dark mode toggle</title>
  <script>
    // Runs before the page is drawn, so there is no flash of the wrong theme.
    const saved = localStorage.getItem("theme");
    const systemDark = matchMedia("(prefers-color-scheme: dark)").matches;
    document.documentElement.setAttribute("data-bs-theme", saved ?? (systemDark ? "dark" : "light"));
  </script>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container py-4">
    <div class="d-flex justify-content-between align-items-center mb-3">
      <h1 class="h4 mb-0">Isha's Study Planner</h1>
      <button type="button" class="btn btn-outline-secondary btn-sm" id="themeBtn">Switch theme</button>
    </div>
    <div class="p-3 bg-body-tertiary border rounded mb-3">
      <p class="mb-1 text-body-emphasis fw-semibold">Today</p>
      <p class="mb-0 text-body-secondary">Physics revision, 2 hours</p>
    </div>
    <div class="p-3 bg-success-subtle text-success-emphasis rounded mb-3">Chemistry chapter 4 complete</div>
    <input class="form-control" placeholder="Add a task">
    <p class="mt-3 small">Current theme: <span id="current"></span></p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    const root = document.documentElement;
    const current = document.getElementById("current");
    current.textContent = root.getAttribute("data-bs-theme");

    document.getElementById("themeBtn").addEventListener("click", () => {
      const next = root.getAttribute("data-bs-theme") === "dark" ? "light" : "dark";
      root.setAttribute("data-bs-theme", next);
      localStorage.setItem("theme", next);
      current.textContent = next;
    });
  </script>
</body>
</html>

Click the button: the page, the panels, the success message and even the input field change together. Run the example again and the page remembers your last choice. Two details are worth copying into your own projects:

  • The small script in the <head> sets the attribute before the page is drawn. If you set it at the end of the body, users with dark mode see a white flash on every page load.
  • saved ?? (...) uses the nullish operator: if nothing is saved, getItem returns null and the system setting is used instead.

One limitation in 5.3: the plain text-{color} and bg-{color} classes keep the same colour in both modes, while the -subtle, -emphasis and body classes adapt. For dark-mode-friendly designs, prefer the adapting ones.

Older code

Before 5.3 there were no colour modes, and you will meet these older patterns:

  • text-muted is deprecated since 5.3; use text-body-secondary. It still works for now but is planned for removal in Bootstrap 6.
  • text-black-50 and text-white-50 are deprecated since 5.1; use a colour with text-opacity-50.
  • navbar-dark, dropdown-menu-dark, carousel-dark and btn-close-white were the per-component way to darken things. In 5.3, data-bs-theme="dark" on the component is preferred for navbars, dropdowns, carousels and close buttons.
  • Bootstrap 4 badges used badge-primary. In Bootstrap 5 you write text-bg-primary on the badge.

Common mistakes

  • bg-dark without a text colour. The text stays dark on a dark background. Use text-bg-dark instead.
  • Setting the theme in a script at the bottom of the page. Users see a flash of the light theme. Set data-bs-theme in the <head>.
  • Hard-coding colours like style="background:#fff". That white box stays white in dark mode. Use bg-body or bg-body-tertiary, which adapt.
  • Using text-primary for long paragraphs. Coloured body text is tiring and often low contrast. Keep colour for short labels and actions.

Key points

  • Theme colours are named by purpose: primary, secondary, success, danger, warning, info, light, dark.
  • text-bg-{color} sets a background plus a readable text colour; -subtle and -emphasis classes give softer shades.
  • body, body-secondary and body-tertiary classes use the page's own colours and adapt to dark mode.
  • data-bs-theme="dark" on <html> or any element switches it to the dark colour mode.
  • A theme toggle sets the attribute in the <head>, follows prefers-color-scheme by default and saves the choice in localStorage.

Exercise

Build a small "Bus pass status" page for Rohtak city buses. Show three status boxes: an active pass (success, subtle style), a pass expiring in 3 days (warning, using text-bg-warning) and a blocked pass (danger, subtle style), each with a short sentence. Add a button that switches between light and dark mode and remembers the choice. Check that all three boxes stay readable in both modes.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Bus pass status</title>
  <script>
    const saved = localStorage.getItem("theme");
    const dark = matchMedia("(prefers-color-scheme: dark)").matches;
    document.documentElement.setAttribute("data-bs-theme", saved ?? (dark ? "dark" : "light"));
  </script>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container py-4">
    <button type="button" class="btn btn-secondary btn-sm mb-3" id="toggle">Light / dark</button>
    <div class="p-3 mb-2 rounded bg-success-subtle text-success-emphasis">Aarav: pass active until 30 November</div>
    <div class="p-3 mb-2 rounded text-bg-warning">Priya: pass expires in 3 days</div>
    <div class="p-3 rounded bg-danger-subtle text-danger-emphasis">Rohan: pass blocked, visit the depot</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    document.getElementById("toggle").addEventListener("click", () => {
      const root = document.documentElement;
      const next = root.getAttribute("data-bs-theme") === "dark" ? "light" : "dark";
      root.setAttribute("data-bs-theme", next);
      localStorage.setItem("theme", next);
    });
  </script>
</body>
</html>

The subtle and emphasis classes switch shades in dark mode automatically, and text-bg-warning always pairs yellow with dark text, so every box stays readable.

Quick Quiz

1. Which class gives a red background with a text colour that is readable on it?

2. How do you switch one card, and only that card, to dark mode in Bootstrap 5.3?

3. Which class replaces the deprecated text-muted?

4. Why is the theme set by a script in the <head> instead of at the end of the <body>?

Learn Bootstrap with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call