Customising with CSS Variables & Sass
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:
<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:
| Variable | Controls |
|---|---|
--bs-body-font-family, --bs-body-font-size | The page font |
--bs-body-color, --bs-body-bg | Main text and background colours |
--bs-border-radius (and -sm, -lg, -xl) | Rounded corners on most components |
--bs-border-color | Default border colour |
--bs-link-color-rgb, --bs-link-hover-color-rgb | Link colours, as R, G, B numbers |
--bs-primary-rgb, --bs-success-rgb... | Colours used by text-* and bg-* utilities |
<!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:
<!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:
// 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 variable | What it changes |
|---|---|
$primary, $secondary, $success... | Theme colours everywhere |
$font-family-sans-serif, $font-size-base | Fonts and base text size |
$border-radius | Corner rounding |
$spacer | The base of the whole spacing scale (default 1rem) |
$grid-breakpoints, $container-max-widths | Breakpoints and container widths |
$enable-shadows, $enable-gradients | Turns on subtle shadows or gradients on components |
$enable-rounded | Set to false for square corners everywhere |
$enable-negative-margins | Generates mt-n1 style classes |
$enable-dark-mode, $color-mode-type | Dark 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:
@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-primaryand 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-rgbvariables expect three numbers,121, 82, 179, because Bootstrap wraps them inrgba(). - 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-bgand--bs-card-border-colorrestyle one component or one element. - Sass variables like
$primaryand$enable-shadows, set before importing Bootstrap, change the whole framework at build time. - Adding to
$theme-colorsand$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
<!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?
In the compiled CSS, each button variant sets fixed values for --bs-btn-*; change those or use Sass.
2. Where must Sass variable overrides such as $primary go?
Bootstrap's variables use !default, so values set earlier take priority.
3. What value does --bs-link-color-rgb expect?
The -rgb variables are placed inside rgba(), so they need the red, green and blue numbers.
4. Which approach lets a user pick a theme colour while the page is open?
Sass runs only when you build; CSS variables can change in the browser at any time.
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
