Quick and Dirty Bootstrap Overrides at Runtime

Avatar of Meredith Matthews
Meredith Matthews on (Updated on )

Bootstrap 5.2 shipped with solutions for the methods covered in this post. So, if you’re using Bootstrap 5.2 or newer, you should be able to tinker with CSS variables right out of the box for custom overrides.

Oh, Bootstrap, that old standard web library that either you hate or you spend all your time defending as “it’s fine, it’s not that bad.” Regardless of what side you fall on, it’s a powerful UI framework that’s everywhere, most people know the basics of it, and it gives you extremely predictable results.

For better or worse, Bootstrap is opinionated. It wants you to construct your HTML a certain way, it wants you to override styles a certain way, it wants to be built from core files a certain way, and it wants to be included in websites a certain way. Most of the time, unless you have a coworker who writes Bootstrap badly, this is fine, but it doesn’t cover all use cases.

Bootstrap wants to be generated server-side and it does not like having its styles overridden at runtime. If you’re in a situation where you want some sort of visual theme feature in your application, what Bootstrap wants you to do is generate separate stylesheets for each theme and swap out stylesheets as you need. This is a great way to do it if you have pre-defined themes you’re offering to users. But what if you want user-defined themes? You could set up your app to run Sass and compile new stylesheets and save them to the server, but that’s a lot of work—plus you have to go talk to the back-end guys and DevOps which is a bunch of hassle if you only want to, say, swap out primary and secondary colors, for example.

So this is where I was.

I’m building a multi-user SaaS app using Django and Vue with a fixed layout, but also a requirement to be able to change the branding colors for each user account with an automatic default color theme. There is another requirement that we don’t re-deploy the app every time a new user is added. And, finally, every single back-end and DevOps dev is currently swamped with other projects, so I have to solve this problem on my own.

Since I really don’t want to compile Sass at runtime, I could just create stylesheets and inject them into pages, but this is a bad solution since we’re focusing on colors. Compiled Bootstrap stylesheets render out the color values as explicit hex values, and (I just checked) there are 23 different instances of primary blue in my stylesheet. I would need to override every instance of that just for primary colors, then do it again for secondary, warning, danger, and all the other conventions and color standardizations we want to change. It’s complicated and a lot of work. I don’t want to do that.

Luckily, this new app doesn’t have a requirement to support Internet Explorer 11, so that means I have CSS variables at my disposal. They’re great, too, and they can be defined after loading a stylesheet, flowing in every direction and changing all the colors I want, right? And Bootstrap generates that big list of variables in the :root element, so this should be simple.

This is when I learned that Bootstrap only renders some of its values as variables in the stylesheet, and that this list of variables is intended entirely for end-user consumption. Most of the variables in that list ate not referenced in the rest of the stylesheet, so redefining them does nothing. (However, it’s worth a note that better variable support at runtime may be coming in the future.)

So what I want is my Bootstrap stylesheet to render with CSS variables that I can manipulate on the server side instead of static color values, and strictly speaking, that’s not possible. Sass won’t compile if you set color variables as CSS variables. There are a couple of clever tricks available to make Sass do this (here’s one, and another), but they require branching Bootstrap, and branching away from the upgrade path introduces a bit of brittleness to my app that I’m unwilling to add. And if I’m perfectly honest, the real reason I didn’t implement those solutions was that I couldn’t figure out how to make any of them work with my Sass compiler. But you might have better luck.

This is where I think it’s worth explaining my preferred workflow. I prefer to run Sass locally on my dev machine to build stylesheets and commit the compiled stylesheets to the repo. Best practices would suggest the stylesheets should be compiled during deployment, and that’s correct, but I work for a growing, perpetually understaffed startup. I work with Sass because I like it, but in what is clearly a theme for my job, I don’t have the time, power or spiritual fortitude to integrate my Sass build with our various deployment pipelines.

It’s also a bit of lawful evil self-defense: I don’t want our full-stack developers to get their mitts on my finely-crafted styles and start writing whatever they want; and I’ve discovered that for some reason they have a terrible time getting Node installed on their laptops. Alas! They just are stuck asking me to do it, and that’s exactly how I want things.

All of which is to say: if I can’t get the stylesheets to render with the variables in it, there’s nothing stopping me from injecting the variables into the stylesheet after it’s been compiled.

Behold the power of find and replace!

What we do is go into Bootstrap and find the colors we want to replace, conveniently found at the top of your compiled stylesheet in the :root style:

:root {
  --bs-blue: #002E6D;
  --bs-indigo: #6610F2;
  --bs-purple: #6F42C1;
  --bs-pink: #E83E8C;
  --bs-red: #DC3545;
  --bs-orange: #F2581C;
  --bs-yellow: #FFC107;
  --bs-green: #28A745;
  --bs-teal: #0C717A;
  --bs-cyan: #007DBC;
  --bs-white: #fff;
  --bs-gray: #6c757d;
  --bs-gray-dark: #343a40;
  --bs-gray-100: #f8f9fa;
  --bs-gray-200: #e9ecef;
  --bs-gray-300: #dee2e6;
  --bs-gray-400: #ced4da;
  --bs-gray-500: #adb5bd;
  --bs-gray-600: #6c757d;
  --bs-gray-700: #495057;
  --bs-gray-800: #343a40;
  --bs-gray-900: #212529;
  --bs-primary: #002E6D;
  --bs-brand: #DC3545;
  --bs-secondary: #495057;
  --bs-success: #28A745;
  --bs-danger: #DC3545;
  --bs-warning: #FFC107;
  --bs-info: #007DBC;
  --bs-light: #fff;
  --bs-dark: #212529;
  --bs-background-color: #e9ecef;
  --bs-bg-light: #f8f9fa;
  --bs-primary-rgb: 13, 110, 253;
  --bs-secondary-rgb: 108, 117, 125;
  --bs-success-rgb: 25, 135, 84;
  --bs-info-rgb: 13, 202, 240;
  --bs-warning-rgb: 255, 193, 7;
  --bs-danger-rgb: 220, 53, 69;
  --bs-light-rgb: 248, 249, 250;
  --bs-dark-rgb: 33, 37, 41;
  --bs-white-rgb: 255, 255, 255;
  --bs-black-rgb: 0, 0, 0;
  --bs-body-rgb: 33, 37, 41;
  --bs-font-sans-serif: system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, Liberation Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
  --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
  --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
  --bs-body-font-family: Source Sans Pro;
  --bs-body-font-size: 1rem;
  --bs-body-font-weight: 400;
  --bs-body-line-height: 1.5;
  --bs-body-color: #212529;
  --bs-body-bg: #e9ecef;
}

Grab the value for, say, --bs-primary, the good ol’ Bootstrap blue. I use Gulp to compile my stylesheets, so let’s take a look at the Sass task function for that in the gulpfile.js:

var gulp = require('gulp');
var sass = require('gulp-sass')(require('sass'));
var sourcemaps = require('gulp-sourcemaps');

function sassCompile() {
  return gulp.src('static/sass/project.scss')
  .pipe(sourcemaps.init())
  .pipe(sass({outputStyle: 'expanded'}))
  .pipe(sourcemaps.write('.'))
  .pipe(gulp.dest('/static/css/'));
}
exports.sass = sassCompile;

I want to copy and replace this color throughout my entire stylesheet with a CSS variable, so I installed gulp-replace to do that. We want our find-and-replace to happen at the very end of the process, after the stylesheet is compiled but before it’s saved. That means we ought to put the pipe at the end of the sequence, like so:

var gulp = require('gulp');
var sass = require('gulp-sass')(require('sass'));
var sourcemaps = require('gulp-sourcemaps');
var gulpreplace = require('gulp-replace');
function sassCompile() {
  return gulp.src('static/sass/project.scss')
    .pipe(sourcemaps.init())
    .pipe(sass({outputStyle: 'expanded'}))
    .pipe(sourcemaps.write('.'))
    .pipe(gulpreplace(/#002E6D/ig, 'var(--ct-primary)'))
    .pipe(gulp.dest('static/css/'));
}
exports.sass = sassCompile; 

Compile the stylesheet, and check it out.

:root {
  --bs-blue: var(--ct-primary);
  --bs-indigo: #6610F2;
  --bs-purple: #6F42C1;
  --bs-pink: #E83E8C;
  --bs-red: #DC3545;
  --bs-orange: #F2581C;
  --bs-yellow: #FFC107;
  --bs-green: #28A745;
  --bs-teal: #0C717A;
  --bs-cyan: #007DBC;
  --bs-white: #fff;
  --bs-gray: #6c757d;
  --bs-gray-dark: #343a40;
  --bs-gray-100: #f8f9fa;
  --bs-gray-200: #e9ecef;
  --bs-gray-300: #dee2e6;
  --bs-gray-400: #ced4da;
  --bs-gray-500: #adb5bd;
  --bs-gray-600: #6c757d;
  --bs-gray-700: #495057;
  --bs-gray-800: #343a40;
  --bs-gray-900: #212529;
  --bs-primary: var(--ct-primary);
  --bs-brand: #DC3545;
  --bs-secondary: #495057;
  --bs-success: #28A745;
  --bs-danger: #DC3545;
  --bs-warning: #FFC107;
  --bs-info: #007DBC;
  --bs-light: #fff;
  --bs-dark: #212529;
  --bs-background-color: #e9ecef;
  --bs-bg-light: #f8f9fa;
  --bs-primary-rgb: 13, 110, 253;
  --bs-secondary-rgb: 108, 117, 125;
  --bs-success-rgb: 25, 135, 84;
  --bs-info-rgb: 13, 202, 240;
  --bs-warning-rgb: 255, 193, 7;
  --bs-danger-rgb: 220, 53, 69;
  --bs-light-rgb: 248, 249, 250;
  --bs-dark-rgb: 33, 37, 41;
  --bs-white-rgb: 255, 255, 255;
  --bs-black-rgb: 0, 0, 0;
  --bs-body-rgb: 33, 37, 41;
  --bs-font-sans-serif: system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, Liberation Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
  --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
  --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
  --bs-body-font-family: Source Sans Pro;
  --bs-body-font-size: 1rem;
  --bs-body-font-weight: 400;
  --bs-body-line-height: 1.5;
  --bs-body-color: #212529;
  --bs-body-bg: #e9ecef;
}

Cool, OK, we now have an entire stylesheet that wants a variable value for blue. Notice it changed both the primary color and the “blue” color. This isn’t a subtle technique. I call it quick-and-dirty for a reason, but it’s fairly easy to get more fine-grained control of your color replacements if you need them. For instance, if you want to keep “blue” and “primary” as separate values, go into your Sass and redefine the $blue and $primary Sass variables into different values, and then you can separately find-and-replace them as needed.

Next, we need to define our new default variable value in the app. It’s as simple as doing this in the HTML head:

<link href="/static/css/project.css" rel="stylesheet">
<style>
  :root {
    --ct-primary: #002E6D;
  }
</style>

Run that and everything shows up. Everything that needs to be blue is blue. Repeat this process a few times, and you suddenly have lots of control over the colors in your Bootstrap stylesheet. These are the variables I’ve chosen to make available to users, along with their default color values:

--ct-primary: #002E6D;
--ct-primary-hover: #00275d;
--ct-secondary: #495057;
--ct-secondary-hover: #3e444a;
--ct-success: #28A745;
--ct-success-hover: #48b461;
--ct-danger: #DC3545;
--ct-danger-hover: #bb2d3b;
--ct-warning: #FFC107;
--ct-warning-hover: #ffca2c;
--ct-info: #007DBC;
--ct-info-hover: #006aa0;
--ct-dark: #212529;
--ct-background-color: #e9ecef;
--ct-bg-light: #f8f9fa;
--bs-primary-rgb: 0, 46, 109;
--bs-secondary-rgb: 73, 80, 87;
--bs-success-rgb: 40, 167, 69;
--bs-info-rgb: 0, 125, 188;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-rgb: 33, 37, 41;

Now the fun begins! From here, you can directly manipulate these defaults if you like, or add a second :root style below the defaults to override only the colors you want. Or do what I do, and put a text field in the user profile that outputs a :root style into your header overriding whatever you need. Voilà, you can now override Bootstrap at runtime without recompiling the stylesheet or losing your mind.

This isn’t an elegant solution, certainly, but it solves a very specific use case that developers have been trying to solve for years now. And until Bootstrap decides it wants to let us easily override variables at runtime, this has proven to be a very effective solution for me.