Customising with CSS Variables & Sass

Bootstrap · Lesson 25 of 28 7 min read Updated

A site built with default Bootstrap looks like thousands of other sites. Clients want their own colours, fonts and corner styles. Bootstrap 5.3 gives you two ways to change its look without editing its files: CSS variables, which you can change in any stylesheet, even with the CDN version, and Sass variables, which change Bootstrap at its source when you build it with npm.

In this lesson you will use both, learn which one to pick for each job, and add your own colour and utility class to Bootstrap.

Rule one: never edit Bootstrap's files

Editing bootstrap.min.css or the files in node_modules/bootstrap seems quick, but your changes vanish the next time you update or reinstall. Always put your changes in your own file, loaded after Bootstrap:

HTML
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="css/site.css" rel="stylesheet">   <!-- your styles win because they come later -->

When both files set the same property with equally specific selectors, the later file wins (see Cascade & Specificity). Do not reach for !important except to override a utility class, which already uses it.

Global CSS variables

Bootstrap defines many CSS custom properties on :root, all starting with --bs-. Components read them, so changing a variable changes everything that uses it:

VariableControls
--bs-body-font-family, --bs-body-font-sizeThe page font
--bs-body-color, --bs-body-bgMain text and background colours
--bs-border-radius (and -sm, -lg, -xl)Rounded corners on most components
--bs-border-colorDefault border colour
--bs-link-color-rgb, --bs-link-hover-color-rgbLink colours, as R, G, B numbers
--bs-primary-rgb, --bs-success-rgb...Colours used by text-* and bg-* 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>CSS variable theming</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    /* Your own stylesheet, loaded after Bootstrap */
    :root {
      --bs-body-font-family: Georgia, "Times New Roman", serif;
      --bs-border-radius: 1rem;
      --bs-primary-rgb: 121, 82, 179;           /* used by text-primary and bg-primary */
      --bs-link-color-rgb: 121, 82, 179;
      --bs-link-hover-color-rgb: 84, 51, 135;
    }
    /* A brand button built from Bootstrap's button variables */
    .btn-brand {
      --bs-btn-bg: #7952b3;
      --bs-btn-border-color: #7952b3;
      --bs-btn-color: #fff;
      --bs-btn-hover-bg: #5f3e94;
      --bs-btn-hover-border-color: #5f3e94;
      --bs-btn-hover-color: #fff;
      --bs-btn-active-bg: #543387;
      --bs-btn-focus-shadow-rgb: 121, 82, 179;
    }
  </style>
</head>
<body>
  <div class="container py-4">
    <h1 class="h4 text-primary">Lucknow Literature Circle</h1>
    <p>Monthly book readings in Hazratganj. <a href="#events">See upcoming events</a>.</p>
    <div class="card mb-3" style="max-width: 420px">
      <div class="card-body">Rounded corners now come from <code>--bs-border-radius</code>.</div>
    </div>
    <button type="button" class="btn btn-brand me-2">Join the circle</button>
    <button type="button" class="btn btn-primary">btn-primary is still blue</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Look closely at the last button. Changing --bs-primary-rgb recoloured the text-primary heading, but btn-primary stayed blue. That is because in the compiled CSS, each button variant stores its own colour values in component variables such as --bs-btn-bg, written as fixed colours, not as references to --bs-primary. So CSS variables are perfect for fonts, corner radius, body colours, links and individual components, but a complete brand-colour change across every component is a job for Sass.

Component variables

Most components have their own variables, set on the component's class: --bs-btn-* for buttons, --bs-card-* for cards, --bs-navbar-* for navbars, --bs-table-* for tables and so on. You can change them for one element with a style attribute, or for a group with your own class:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Component variables</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    /* All cards in the "pricing" section get a thicker purple border */
    .pricing .card {
      --bs-card-border-color: #7952b3;
      --bs-card-border-width: 2px;
      --bs-card-cap-bg: #f3effa;     /* card-header background */
    }
    /* A navbar with custom link colours */
    .navbar-festive {
      --bs-navbar-color: #fff8e1;
      --bs-navbar-hover-color: #ffd54f;
      --bs-navbar-brand-color: #ffffff;
      background: #b5179e;
    }
  </style>
</head>
<body>
  <nav class="navbar navbar-festive">
    <div class="container">
      <a class="navbar-brand" href="#home">Diwali Mela, Jaipur</a>
      <div class="navbar-nav"><a class="nav-link" href="#stalls">Stalls</a></div>
    </div>
  </nav>
  <div class="container py-3">
    <div class="row g-3 pricing">
      <div class="col-sm-6"><div class="card"><div class="card-header">Stall, 1 day</div><div class="card-body">₹1,500</div></div></div>
      <div class="col-sm-6"><div class="card"><div class="card-header">Stall, 3 days</div><div class="card-body">₹3,900</div></div></div>
    </div>
    <div class="card mt-3" style="--bs-card-bg: #fff3cd;"><div class="card-body">One card changed with a style attribute.</div></div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

To find a component's variable names, open its docs page on getbootstrap.com (most have a "CSS variables" section) or inspect the element in your browser's developer tools.

Customising with Sass

Sass is a CSS preprocessor: you write .scss files with variables, maps and functions, and a compiler turns them into normal CSS (see Sass). Bootstrap is written in Sass, and every Sass variable is marked !default, which means "use this value unless one was already set". So if you set a variable before Bootstrap's own files are imported, your value wins, and every component built from it changes.

The simplest setup, in a Vite or other npm project with the sass package installed:

SCSS
// src/styles.scss
$primary: #7952b3;
$font-family-sans-serif: Georgia, "Times New Roman", serif;
$border-radius: .75rem;
$enable-shadows: true;

@import "bootstrap/scss/bootstrap";

Now btn-primary, links, focus rings, form switches, pagination and everything else use the new purple, including the hover and pressed shades that Bootstrap calculates from $primary. Some useful settings:

Sass variableWhat it changes
$primary, $secondary, $success...Theme colours everywhere
$font-family-sans-serif, $font-size-baseFonts and base text size
$border-radiusCorner rounding
$spacerThe base of the whole spacing scale (default 1rem)
$grid-breakpoints, $container-max-widthsBreakpoints and container widths
$enable-shadows, $enable-gradientsTurns on subtle shadows or gradients on components
$enable-roundedSet to false for square corners everywhere
$enable-negative-marginsGenerates mt-n1 style classes
$enable-dark-mode, $color-mode-typeDark mode on or off; data attribute or media-query

Adding a theme colour and a utility

To add a completely new colour name, you must work with the maps, so the import order matters. Import the functions first, set variables, import the variables, then change maps, then import the rest:

SCSS
@import "bootstrap/scss/functions";

$primary: #7952b3;                      // variable overrides go here

@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";

$theme-colors: map-merge($theme-colors, ("saffron": #ff9933));   // map changes go here

@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";

// The utilities API: add .ls-tight and .ls-wide for letter spacing
$utilities: map-merge($utilities, (
  "letter-spacing": (
    property: letter-spacing,
    class: ls,
    values: (tight: -.02em, wide: .1em)
  )
));

@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/helpers";
@import "bootstrap/scss/utilities/api";

This one map entry generates btn-saffron, btn-outline-saffron, text-bg-saffron, bg-saffron, text-saffron, border-saffron, alert-saffron (if you import alerts) and more. The $utilities entry creates ls-tight and ls-wide; add responsive: true to the entry to also get ls-md-wide and the other breakpoint versions.

Importing only the parts you need, as above, also makes the final CSS smaller. Add more lines (such as forms, nav, navbar, card, modal) for the components your site uses.

Which method should I use?

  • Only the CDN, small changes: CSS variables in your own stylesheet.
  • One component looks different: component variables (--bs-card-*, --bs-btn-*) in a class of your own.
  • A real brand theme, new colour names, smaller files: Sass with npm and a build tool.
  • Users choose a theme at runtime (for example a colour picker): CSS variables, because Sass runs only at build time.

Common mistakes

  • Changing --bs-primary and expecting buttons to change. Buttons use their own variables. Use Sass, or set --bs-btn-* variables.
  • Setting Sass variables after @import "bootstrap/scss/bootstrap". By then the CSS has been generated with the defaults. Overrides must come first.
  • Writing --bs-primary-rgb: #7952b3. The -rgb variables expect three numbers, 121, 82, 179, because Bootstrap wraps them in rgba().
  • Loading your stylesheet before Bootstrap's. Bootstrap's rules then win. Load yours last.

Key points

  • Never edit Bootstrap's files; load your own stylesheet after Bootstrap.
  • Global --bs-* variables change fonts, body colours, radius, links and utility colours, even with the CDN build.
  • Component variables such as --bs-btn-bg and --bs-card-border-color restyle one component or one element.
  • Sass variables like $primary and $enable-shadows, set before importing Bootstrap, change the whole framework at build time.
  • Adding to $theme-colors and $utilities (with the correct import order) generates new colour classes and utility classes.

Exercise

Using only CSS variables (no Sass), restyle a page for "Kerala Ayurveda Centre": use a serif font for the body, square corners everywhere (--bs-border-radius of 0), green links (RGB 25, 135, 84), and create a btn-leaf button class with a green background (#198754), a darker green hover (#146c43) and white text. Show a card, a link and both a btn-leaf and a btn-outline-secondary button.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Kerala Ayurveda Centre</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    :root {
      --bs-body-font-family: Georgia, serif;
      --bs-border-radius: 0;
      --bs-link-color-rgb: 25, 135, 84;
      --bs-link-hover-color-rgb: 20, 108, 67;
    }
    .btn-leaf {
      --bs-btn-bg: #198754;
      --bs-btn-border-color: #198754;
      --bs-btn-color: #fff;
      --bs-btn-hover-bg: #146c43;
      --bs-btn-hover-border-color: #146c43;
      --bs-btn-hover-color: #fff;
    }
  </style>
</head>
<body>
  <div class="container py-4">
    <div class="card mb-3" style="max-width: 420px">
      <div class="card-body">
        <h1 class="card-title h5">Kerala Ayurveda Centre</h1>
        <p class="card-text">Traditional treatments in Kochi. <a href="#treatments">View treatments</a></p>
        <button type="button" class="btn btn-leaf me-2">Book a consultation</button>
        <button type="button" class="btn btn-outline-secondary">Call us</button>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

The :root variables change the font, corners and links for the whole page, and btn-leaf sets Bootstrap's own button variables, so it behaves like any other button variant.

Quick Quiz

1. Why does setting --bs-primary in your CSS not change the colour of btn-primary?

2. Where must Sass variable overrides such as $primary go?

3. What value does --bs-link-color-rgb expect?

4. Which approach lets a user pick a theme colour while the page is open?

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