Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 185 of X: Customising Bootstrap 5 with Sass (CSS)

06 Jun 2026

This is the final instalment of our initial overview of Sass, but it’s also laying a strong foundation for our return to developing statically generated websites with Jekyll. Why? Because Sass is natively supported by Jekyll. But the foundation we’re laying in this instalment is even stronger, because we’ll be exploring how to use Sass to deeply customise and integrate Bootstrap into our styles. We’ve relied on Bootstrap through this series, and we’ll continue to do so when we return our focus to Jekyll.

This instalment will take a familiar form — starting with an overview describing the different techniques that can be used to customise Bootstrap with Sass, and then tying it all together with a worked example.

Matching Podcast Episode

You can also Download the MP3

Read an unedited, auto-generated transcript with chapter marks: PBS_2026_06_06

Instalment Resources

Overview

To customise Bootstrap using Sass, you need to import the Bootstrap Sass code into your own Sass code. Doing so opens up a number of different avenues for customisation:

  1. Overriding Bootstrap variables
    1. Optional features can be enabled or disabled
    2. Variables controlling the implementation of the standard Boostrap styles can be altered
    3. Bootstrap’s maps can be altered to add or remove style variations
  2. Applying Bootstrap styles to your own selectors using @extend
  3. Applying customised versions of Boostrap’s styles to your own selector using Bootstrap mixins
  4. For very advanced uses, Bootstrap also provides an API you can consume. However, this is beyond the scope of this series.

In addition to facilitating customisation in these ways, Bootstrap also provides some useful helper functions you can use within your own Sass code.

We’ll describe each of these in detail shortly, but before we do we need to lay a little groundwork.

Importing Bootstrap

Surprisingly, this seemingly trivial step is actually one of the most complex. Why? Because to achieve deeper customisations you can’t simply import all of Bootstrap at once. Thankfully, the simpler your needs, the simpler the import process.

There are three approaches you can take:

  1. Simplest — a monolithic import, which has four caveats:
    1. You can’t use Bootstrap’s utility functions while overriding Bootstrap’s variables (but you can later in your code)
    2. You can’t alter Bootstrap’s maps
    3. You can’t use the Bootstrap API
    4. You import all of Bootstrap, even the parts you don’t need
  2. Typical — a three-step import, which has just two caveats:
    1. You can’t use the Bootstrap API
    2. You import all of Bootstrap, even the parts you don’t use
  3. Fully Customisable — this has no caveats, but it’s quite laborious!

The third option is so laborious, and so seldom needed, that we won’t describe it in this series. If you really need to go to that level, you’ll find guidance in the Bootstrap documentation.

For our worked example, we’ll use the second option, which is the most commonly used because it strikes a nice balance between capability and complexity.

Skeleton 1 — Simplest

If you don’t need to alter Bootstrap’s maps, you don’t need to make use of any Bootstrap functions when overriding Bootstrap variables, and you won’t be using the API, so your Sass code should implement this simple structure:

// Step 1 — override Bootstrap variables here
// CAVEATS:
// 1. you can't customise Bootstrap's maps with this approach
// 2. you can't use the functions Bootstrap provides while overriding the variables
// 3. you can't use the Bootstrap API
// …

// Step 2 — import Bootstrap all at once
@import "bootstrap"; // exact path will vary depending on how you installed Bootstrap

// Step 3 — add your own Sass code, which can make use of:
// 1. Bootstrap's variables (for reading at this stage)
// 2. Bootstrap's maps
// 3. Bootstrap's functions
// 4. Bootstrap's mixins
// 5. Boostrap's classes
// …

Skeleton 2 — Typical

There are lots of really good reasons to want to customise Bootstrap’s maps, and it can very convenient to have access to Bootstrap’s helper functions while doing your variable overrides, so this approach strikes a great balance between functionality and complexity:

// Step 1 — import just the Bootstrap functions
@import "functions"; // exact path will vary depending on how you installed Bootstrap

// Step 2 — override Bootstrap variables here
// …

// Step 3 — import Bootstrap's variables
@import "variables"; // exact path will vary depending on how you installed Bootstrap
@import "variables-dark"; // optional, skip if you don't need dark mode

// Step 4 — edit Bootstrap maps here
// …

// Step 5 — Include the rest of Bootstrap
@import "bootstrap" // exact path will vary depending on how you installed Bootstrap
  
// Step 6  add your own Sass code, which can make use of:
// 1. Bootstrap's variables (for reading at this stage)
// 2. Bootstrap's maps
// 3. Bootstrap's functions
// 4. Bootstrap's mixins
// 5. Boostrap's classes
// 6. the Bootstrap API
// …

Bootstrap’s Sass Documentation

Bootstrap’s documentation does contain an overview of the Sass functionality under the Customise heading in the main sidebar, but for the most part, the Sass documentation is spread throughout Bootstrap’s documentation. For example, if you want to understand how Sass can be used to customise the grid, you’ll find Sass-related sections in the Grid documentation, specifically, Sass variables and Sass Mixins. The same pattern is repeated for every other Bootstrap concept and feature that support Sass customisation. The headings you generally see are Sass Variables, Sass Mixins, Sass Maps, occasionally Sass Loops, and the odd mention of the Bootstrap API.

Looking at the Sass-related subsections throughout the documentation, the big-picture comes into focus:

  1. Not all Bootstrap features can be customised with Sass — you can customise many more than with CSS variables, but you still can’t customise absolutely everything.
  2. The vast majority of the customisation options are exposed via simple Sass variables. This is why the basic import is often all you need.
  3. After regular variables, the next most common customisation method is mixins.
  4. Numerically, there are very few Sass maps, so you won’t see that subsection often. However, their power gives them a higher importance than their scarcity suggests!
  5. There are just occasional mentions of Sass Loops, and these are usually only included to help explain how the maps get translated into Bootstrap classes.
  6. As few mentions as there are of loops, there are even fewer of the API.

Bootstrap’s Sass documentation is unusual in that it very often starts by showing you sections of the Bootstrap Sass source code, and only then showing example usages. Initially, I found this very confusing, because I subconsciously interpret code snippets in documentation as examples. Until I understood what the documentation was doing, I was often more confused than informed when reading the docs! On reflection, this is a very sensible approach, because it offers some real advantages to developers:

  1. Variables are usually documented by showing their declaration; immediately showing their initial values.
  2. Mixins are usually documented by showing their declarations first, followed by example usage. This makes the exact meaning of each each argument completely explicit. You don’t need to interpret English verbiage to understand what the mixin does, you can see the actual Sass code!
  3. For loops, the concept of an examples doesn’t even make sense — these loops are part of Bootstrap’s source code; you don’t replicate them! Instead, you leverage them by editing maps. Seeing how Bootstrap loops over maps makes their purpose explicitly clear.This is why maps and loops are usually documented together.

Finally, there are two additional stand-alone documentation pages/sections I want to draw your attention to:

  1. There are so few functions that they only appear once in the documentation, and even then, they don’t even get an entire page, just a section of the Sass overview page. The fact that there are so few doesn’t mean they’re not extremely useful, though!
  2. One special class of variable gets it’s own dedicated page — the option variables.

Customisation Avenues

As you start to customise Bootstrap, you have four primary mechanisms at your disposal:

  1. You can override Bootstrap’s options variables to enable or disable optional features, or to alter Bootstrap’s overall configuration in far-reaching ways.
  2. You can override Bootstrap’s regular variables to affect the behaviour of specific features and components.
  3. You can alter Bootstrap’s maps using the Sass’s built-in map manipulation functions (see previous instalment) to add or remove key-value pairs, or edit the values of existing pairs. This can have far-reaching effects on the CSS classes generated by Bootstrap.
  4. You can inherit Bootstrap’s styles directly into your own CSS selectors using the Sass @extend at-rule.
  5. You can apply customised versions of Bootstrap’s styles to your own CSS selectors using Bootstrap mixins.

The third point deserves some special attention. When you’re using Bootstrap to style a site or an app as you build the site, adding the standard Bootstrap classes is usually not too burdensome. It might get repetitive, and you might find it annoying when you need more than three classes applied to a single tag to achieve your goals, but it’s usually a small price to pay for the immense value Bootstrap brings to your projects. But, there are two common scenarios where adding those classes is not practical, or perhaps even possible!

  1. When you need to use Bootstrap to refresh the look of an existing site or app.
  2. When the HTML you’re styling is auto-generated by a content management system.

If your content management system lets authors compose text in a non-HTML format like Markdown, then there’s no practical way of adding Bootstrap classes! This is true regardless of whether you’re using a real-time CMS like Wordpress, or a static site generator like Jekyll.

It’s this need to style generated code on Jekyll-based sites with Bootstrap that forced me to learn Sass.

Regardless of which subset of those five customisation avenues you use though, you’re just writing Sass code — there is no Bootstrap-specific Sass syntax. It really is just someone else’s Sass code imported into your Sass code to give you a solid base to build on!

So, because there’s no such thing as Bootstrap-specific Sass, the best way to illustrate the possibilities is with a worked example.

Worked Example

To play along with this worked example, start by creating an empty folder and opening a terminal in that folder.

To customise Bootstrap we need a copy of the Bootstrap source code. The simplest way to get that is via the Node JS Package Manager (NPM).

In your folder, run the command:

npm install bootstrap

Bootstrap has now been installed in ./node_modules/bootstrap. For our purposes, there were just two relevant resources installed:

  1. A copy of the Bootstrap Javascript in ./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js
  2. A copy of the Bootstrap Sass code in the folder ./node_modules/bootstrap/scss

Create a new file in your folder named index.scss with the following content:

// ADD NEEDED Sass VARIABLES HERE

// -- Import the Boostrap functions --
@import "./node_modules/bootstrap/scss/functions";

// ADD BOOTSTRAP VARIABLE OVERRIDES HERE

// -- import Bootstrap's variables (including maps) --
@import "./node_modules/bootstrap/scss/variables";

// UPDATE BOOTSTRAP MAPS HERE

// -- Import the rest of Bootstrap --
@import "./node_modules/bootstrap/scss/bootstrap";

// ADD CUSTOM STYLES HERE

This imports Bootstrap into our Sass file without making any customisations. Before we add an HTML page to style, let’s compile our Sass file in watch mode and leave that running:

npx sass --watch index.scss:index.css

As of June 2026, this triggers many deprecation warnings, but the Bootstrap documentation states that these are expected, for now. This means we can safely suppress them.

Kill the watching Sass instance with ctrl+c, then re-start it with the --silence-deprecation flag and the needed deprecation IDs to remove all that clutter:

npx sass --watch index.scss:index.css --silence-deprecation import,global-builtin,color-functions,if-function

We’re now ready to add an HTML page. Copy the file index.html from the instalment ZIP into your folder, then open it in your favourite browser.

You’ll see that we have a fresh copy of the our worked example from the previous instalment, with just one alteration — it now also imports the Bootstrap Javascript code at the end of the body:

<!-- Import the Bootstrap JavaScript bundle -->
<script src="./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>

Open index.html in your favourite code editor to see it’s content, or, review the worked example from the previous instalment for a summary of the file’s big-picture structure.

Before we continue, I want to draw your attention to an important point — the HTML in this file has not been altered to add the usual Bootstrap classes!

When using the default version of Bootstrap, or even a version customised with CSS custom properties, you must update your page’s markup to include the Bootstrap classes. But when you use Sass to customise Bootstrap, you don’t! This allows for better separation between markup and styling.

Before we start our customisations, open index.html in your favourite browser to see our starting point.

Note that the page already has some default Bootstrap styling because our Sass file imports the Bootstrap Sass code which has been compiled into index.css by the Sass compiler.

Step 1 — Declaring our own Sass Variables

Just because we’ll be using Bootstrap to help us style this page doesn’t change the fact that we should collect our own information together at the top of the file in sensibly named variables. This will still help us make our code clearer, easier to write, easier to debug, and easier to maintain!

When developing worked examples I prefer to use the PBS brand colours and my base, but in this case, just like I chose to do in instalment 183, I’ve chosen to use the Bartificer Creations corporate style instead. Why? For the same reason as last time, it was designed to be Bootstrap-compatible by the design company.

Add the following to the top of index.scss:

//
// === Define Our Own Sass Variables First ===
//

// Capture the Bartificer logo colours
$bartificer-logo-green:      #93c01f;
$bartificer-logo-orange:     #f29100;
$bartificer-logo-background: #faf8f0;

// Capture the light version of the Bartificer colour palette
$bartificer-colors-light: (
  background:          #fff,
  header:              #000,
  subheader:           #5f5f5b,
  accent:              #ff7626,
  note:                #9c968c,
  handwriting:         #5f5f5b,
  text:                #000,
  code-text:           #fff,
  code-background:     #5f5f5b,
  action:              #1da6e5,
  action-background:   #dff1fa,
  secondary:           #8c9daf,
  secondary-background:#e6ebf2,
  success:             #78a046,
  success-background:  #dcf0c1,
  warning:             #f0c600,
  warning-background:  #fff2b6,
  error:               #dc5a3c,
  error-background:    #f5cdc2
);

// import the Bartificer fonts from Google Fonts
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900&family=Georama:ital,wght@0,100..900;1,100..900&family=Reenie+Beanie&family=Saira+Semi+Condensed:wght@100;200;300;400;500;600;700;800;900&family=Saira:ital,wght@0,100..900;1,100..900&family=Victor+Mono:ital,wght@0,100..700;1,100..700&display=swap');

// capture the bartificer font families in a Sass map
$bartificer-fontfamilies: (
  body: ("Georama", sans-serif),
  header: ("Saira Semi Condensed", sans-serif),
  subheader: ("Saira", sans-serif),
  handwriting: ("Reenie Beanie", cursive),
  code: ("Victor Mono", monospace),
  logo: ("Exo 2", sans-serif)
);

//
// === Customise Bootstrap ===
//

Note that this is the standard suite of variables I add to the stop of all Sass-based projects under the Bartificer umbrella. It captures all the various colours and so-forth, not just the ones I know I’ll need when I start this project. This is convenient rather than wasteful, because none of these variables will appear in the generated CSS, which is the only thing browsers will be downloading! I also chose to add the colours and fonts in maps, and I chose the key-names within those maps very deliberately — they align with the content of the corporate style guide, making it easier to reference as I code.

Save the file and wait for your watching Sass command to re-build index.css. Refresh the browser, and as expected, you’ll see no change at all, because none of these variables have been used yet!

Step 2 — Override Bootstrap Variables

Now that we’ve captured our own information in sensibly named variables, we’re ready to start overriding Bootstrap variables.

I generally don’t change any option variables because I agree with Bootstrap’s choices for their default values, but for the purpose of this example, we’ll enable the optional, and very subtle, gradient support on backgrounds. As a reminder, there’s a convenient table listing all the supported options along with their default values in the documentation.

In terms of overriding variables, we’ll focus on typography and colour, apply the Bartificer font families and colours to Bootstrap.

Replace the // ADD BOOTSTRAP VARIABLE OVERRIDES HERE placeholder with the following code:

// -- Set Bootstrap options --

// enable Boostrap's optional support for background gradients
$enable-gradients: true;

// -- Override Bootstrap Variables --

// Bootstrap typography variables
$font-family-sans-serif: map.get($bartificer-fontfamilies, body);
$font-family-monospace: map.get($bartificer-fontfamilies, code);
$headings-font-family: map.get($bartificer-fontfamilies, header);
$display-font-family: map.get($bartificer-fontfamilies, logo);

// Bootstrap colour variables
$body-color:   map-get($bartificer-colors-light, text);
$body-bg:      map-get($bartificer-colors-light, background);
$primary:      map-get($bartificer-colors-light, action);
$secondary:    map-get($bartificer-colors-light, secondary);
$success:      map-get($bartificer-colors-light, success);
$info:         map-get($bartificer-colors-light, secondary-background);
$warning:      map-get($bartificer-colors-light, warning);
$danger:       map-get($bartificer-colors-light, error);
$light:        map-get($bartificer-colors-light, note);
$dark:         map-get($bartificer-colors-light, subheader);
$code-color:   map-get($bartificer-colors-light, accent);

As mentioned before, there’s no page in the documentation that lists all the variables, instead, their descriptions are spread throughout the documentation on the pages related to the features they affect. However, if you want to see them all at once, you’ll find them in the file _variables.scss in the Bootstrap source code. (I usually keep this file open in my IDE so I can easily search it.)

After overriding the non-map variables we need to import Boostrap’s variables into our code. This is important, because doing it in any other order would not work as expected. If you browse the _variables.scss file you’ll see why:

  1. The Bootstrap code is written on the expectation that you’ll define your overrides first, this is why all the variable definitions use the !default syntax (e.g. $blue: #0d6efd !default;). This means that any variable that already has a value will keep that value, but those that don’t exist yet will be created with the specified values.
  2. Many variables defined higher up the file are used as default values for other variables lower down the file (e.g. $color-contrast-dark: $black !default;). This means that if you were to override the variable after importing the file it would be the default value that rippled down into all the other variables, not your value!
  3. Similarly, many variables defined higher up in the file are used to calculate values for other variables lower down in the file (e.g. $blue-100: tint-color($blue, 80%) !default;). Again, overriding the variable after the import would result in the default value being used as the basis for all those calculations rather than yours!

Leave the following lines as they are so that the variable import happens:

// -- import Bootstrap's variables (including maps) --
@import "./node_modules/bootstrap/scss/variables";

At this point all the Bootstrap variables exist, and any we chose to override have retained our desired values, and our desired values also rippled down into all they other variables they should have rippled down into. This means that our choice of warning colour was used to calculate all the variants of that colour, and so on.

The Bootstrap maps have now also been declared, which means we can now alter them. However, to do that, we need to import the standard sass:map module!.

Add the following to the top of the file:

//
// == Import the needed standard Sass modules ==
//

// needed to update Bootstrap's Sass maps
@use "sass:map";

We’re now ready to update the maps. For this example we’ll update just one map, the one that controls the spacing-related Bootstrap classes like p-5, mt-4, py-lg-3 etc..

This map is described in the documentation for the spacing utilities, and has the following default value:

$spacer: 1rem;
$spacers: (
  0: 0,
  1: $spacer * .25,
  2: $spacer * .5,
  3: $spacer,
  4: $spacer * 1.5,
  5: $spacer * 3,
);

While Boostrap’s p-5 and m-5 add quite a lot of spacing padding, it’s only 3rem, and I want the option of having even more spacing! To do that, we need to add a sixth name-value pair to the map. This pair will have the name 6, and the value $spacer *6 (this fits the pattern of 5 being double 4).

Replace the // UPDATE BOOTSTRAP MAPS HERE placeholder with the following code:

// -- Update Bootstrap maps --

// add an additional spacer size, creating p-6, m-6, etc.
$spacers: map.set($spacers, 6, $spacer * 6);

Looking at the file after making this addition, notice that the lines of code following this insertion are:

// -- Import the rest of Bootstrap --
@import "./node_modules/bootstrap/scss/bootstrap";

That line imports the remainder of the Bootstrap code, which includes the Bootstrap loops. Some of those loops process the $spacers map, and when they do, they’ll process our extra key-value pair, generating a whole suite of non-standard Bootstrap classes. When you think about it, this really is a lot of classes — every p and m variant that exists for the numbers from 1 to 5 by default, now exists for 6. That includes the two simplest spacing classes p-6 & m-6, but also all the side-specific variations like pt-6 for top paddings, and all the break-point-specific variations like m-md-6 for the medium breakpoint!

Save the file and allow the stylesheet to re-build, then refresh your browser.

The only visual change you’ll see is the fonts.

Step 3 — Apply the Bootstrap Styles

As mentioned before, a significant difference between working with Bootstrap through Sass versus just importing the default version or altering that default version with CSS custom properties, is that you don’t need to alter your markup, or at least not nearly as much. Sometimes it’s easier to just add the standard Bootstrap classes, but often, it just isn’t.

Sass provides us two useful mechanisms for applying Bootstrap styles without adding Bootstrap classes to the markup — mixins, and the @extend Sass at-rule.

Using Bootstrap Mixins

The most glaring issue on our page as it stands now is the spacing. If we were working with default Bootstrap, we’d address this by adding .container classes to the top-level elements on our page. But with Sass we can do better.

What we’d like to achieve is for the page to behave like a typical .container-fluid container until we get to the xl breakpoint, then we’d like the content width to freeze the margins left and right to expand so the content stays centred.

To achieve this we’ll make use of two Bootstrap mixins, and a Bootstrap map:

  1. We’ll use the make-container mixin to make our top-level tags (<header> & <main>) default to behaving as if we had added the .container-fluid class to their markup.
  2. We’ll use the media-breakpoint-up mixin to override this behaviour for all breakpoints from xl up.
  3. We’ll use the $container-max-widths map to access the appropriate maximum width for xl containers.

Replace the // ADD CUSTOM STYLES HERE placeholder at the bottom with the following code:

//
// === Apply Bootstrap Styles ===
//

/* -- Apply top-level spacing -- */

// Use Bootstrap's make-header mixins to make top-level containers fluid until they reach the extra large breakpoint
header, main {
  @include make-container; // convert the header and main tags to fluid containers
}

// use Bootrap media mixin to create a block that only applies from the xl breakpoint up
@include media-breakpoint-up(xl) {
  header, main {
    @include make-container; // convert the header and main tags to fluid containers
    max-width: map.get($container-max-widths, xl); // force the width of the xl breakpoint
  }
}

Allow stylesheet to rebuild, and then refresh the browser. We now have our desired behaviour!

This works because of how Bootstrap breakpoints work — the default breakpoint starts at a width of 0px, and is applied until a more specific condition is met. This is why we start by defining our desired default behaviour:

header, main {
  @include make-container; // convert the header and main tags to fluid containers
}

The make-container mixin is documented on the Containers page in the Bootstrap documentation. It’s used to import the behaviour from .container-fluid into any selector. In this case we’re using it to make the <header> and <main> tags behave like <header class="container-fluid"> and <main class="header-fluid"> tags. As you can see in the docs, the mixin supports one optional argument, to control gutters within the container, but we don’t need to do that, so we can @include the mixin without any arguments.

Next, we need to make an exception for the extra large breakpoint and all breakpoints wider than that. To that we leverage the media-breakpoint-up mixin documented in the Breakpoints page of the docs. This mixin requires a breakpoint name as an argument, so we pass it xl. This allows us to create a block within our code that only applies from the large breakpoint up.

If you’re curious about how the mixin achieves that, the key is CSS’s @media at-rule. We’ve not covered this at-rule in this series, because Boostrap has always handled media queries for is. If you want to learn more, you’ll find a good description on the Mozilla Developer Network.

Finally, within our new break-point-specific code block we want to freeze the width at Bootstrap’s default width for xl containers. We could find that value in the documentation and hard-code it into our code, but that would definitely count as a software engineering bad smell. Why? Because that default value can be overridden by editing a Bootstrap map! We want to be sure our custom behaviour aligns itself with the rest of Bootstrap. The correct solution is to use the value in the Bootstrap map. This is why we explicitly set the max-width CSS style property to the value of the xl key in the $container-max-widths Bootstrap map with:

max-width: map.get($container-max-widths, lg); // force the width of the lg breakpoint

The $container-max-widths is documented on the Breakpoints page.

Using @extend

Most of the time, mixins are over-kill — the standard Sass @extend at-rule for selector inheritance usually lets us get to our desired behaviour more simply.

Caveat: @extend can’t be used within CSS @media blocks like those created by the media-breakpoint-up mixin. This means that within break-point-specific blocks you have to rely exclusively on mixins. My advice is the minimise your use of the media breakpoint mixins.

With our big-picture spacing taken care of, let’s start working our way through the two top-level regions of our page. We’ll start with the header.

Firstly, let’s make the page title behave like a Bootstrap display header, equivalent to the .display-1 CSS class. We can do this by inheriting Bootstrap’s .display-1 class into <h1> tags inside <header> tags:

header h1 {
  @extend .display-1;
}

We also want to use Boostrap’s border, background, and typography classes to convert the page instructions section within the heading area into a card-like region with a subtle background. To help them feel optional when viewing the page, we’ll also shrink the text a little.

We can do that as follows:

header #instructions {
  @extend .border;
  @extend .rounded-3;
  @extend .bg-light-subtle;
  @extend .p-3;
  @extend .small;

  .lead {
    @extend .small;
    @extend .fw-semibold;
  }
  
  pre {
    @extend .border-start;
    @extend .border-3;
    @extend .border-secondary;
    @extend .rounded-end-3;
    @extend .bg-secondary-subtle;
    @extend .ms-3;
    @extend .ps-3;
    @extend .py-1;
  }
}

However, the previous two code snippets are inefficient! Let’s consolidate them a little to give us more elegant and easier-to-maintain code.

Add the following to the bottom of the file:

/* -- Style the header region -- */
header {

  /* Convert the page heading to a display heading */
  h1 {
    @extend .display-1;
  }

  /* Display the instrunctions in a card-like way */
  #instructions {
    @extend .border;
    @extend .rounded-3;
    @extend .bg-light-subtle;
    @extend .p-3;
    @extend .small;

    /* help the lead span stand out appropriately */
    .lead {
      @extend .small;
      @extend .fw-semibold;
    }

    /* Help the code block stand out */
    pre {
      @extend .border-start;
      @extend .border-3;
      @extend .border-secondary;
      @extend .rounded-end-3;
      @extend .bg-secondary-subtle;
      @extend .ms-3;
      @extend .ps-3;
      @extend .py-1;
    }
  }
}

Allow the style to rebuild, then refresh your browser. That header now looks just like it would had we added all those CSS classes to our HTML markup.

We can repeat this process for the wisdom and humour articles in the main content region.

Add the following to the bottom of the file:

/* -- Style the main region -- */
main {
  /* add the needed vertical spacing */
  @extend .pt-5;
  article {
    @extend .mb-5;
  }

  /** style the wisdom article */
  article#wisdom {
    /* Style the wise quotations */
    blockquote {
      @extend .blockquote; // apply the default Bootstrap blockquote styling
      @extend .mx-auto;
      @extend .w-75;
      @extend .my-5;
      @extend .p-6; // apply our custom extra-wide padding
      @extend .border;
      @extend .rounded-5;
      @extend .bg-info-subtle;
      @extend .bg-gradient; // make use of the optional gradient feature
      @extend .fs-1;
      @extend .fw-light;
      @extend .fst-italic;

      /* style the paragraphs */
      p {
        @extend .m-0;
      }

      /* style the attributions */
      p.author {
        @extend .ms-6; // apply our custom extra-wide left margin
        @extend .small;
        @extend .text-uppercase;
        @extend .fw-bold;
        @extend .fs-3;
        @extend .fst-normal;
        .description {
          @extend .small;
          @extend .text-lowercase;
          @extend .text-muted;
          @extend .fw-normal;
        }
      }
    }
  }

  /** style the humour article */
  article#humour {
    /* style the description list */
    dl {
      /* style the quotations themselves */
      dd {
        /* add some left margin */
        @extend .ms-3;

        /* style the text */
        @extend .fw-light;
        @extend .fst-italic;
      }
    }
  }
}

The key point to note here is that all we’ve done here is apply Bootstrap classes via Sass rather than by editing the HTML markup.

At this point, all the customisations we made to Bootstrap have been applied to our page. To keep things simple, we haven’t made that many customisations. In a real-world example like the Let’s Talk Podcasts Jekyll theme there will be many more customisations made to the Bootstrap styles. The effect of adding a few more Bootstrap customisations adds up really fast, tweaking just one or two more variables, editing just one or two more maps, they get amplified, giving you a lot of proverbial bang for your proverbial buck!

However, just like when working with the default version of Bootstrap, you’re going to need more styles that those Boostrap provides. After all, Bootstrap is intended as a powerful starting point, not as a complete solution that can style everything in every possible way!

So, the final step in a Bootstrap-based style sheet is to add the other style declarations that are needed to achieve your desired end result.

Step 4 — Fine-tune with Custom Styles

Now that we’ve gotten as far as we can with Bootstrap, let’s finish things off with a few additional style declarations of our own.

Even thought we’re not technically customising or applying Bootstrap anymore, we can still make use of all those Bootstrap functions, mixins, variables, and classes within our own style declarations!

In this worked example we’re going to keep things simple — we’re going to make just two final tweaks:

  1. We’re going to add some colour to the article headers
  2. We’re going to customise the presentation of the quotations

Add the following to the bottom of the file:

//
// === Define Custom Styles ===
//

/* finesse the article titles */
article h1 {
  color: $bartificer-logo-green;
}

/* finess the quotations */
article#wisdom blockquote, article#humour dd {
  font-family: map.get($bartificer-fontfamilies, handwriting);
  p.author {
    font-family: map.get($bartificer-fontfamilies, body);
  }
}

At this point we could continue to fine-tune the styling for as long as we have the energy, but let’s not over-complicate this worked example!

Allow the style to rebuild one last time, then refresh the browser to see our green article headings and cursive quotations.

Beyond the Worked Example

When creating worked examples I try to be as realistic as possible, but sometimes I have to make compromises for clarity.

The worked example above contains a significant compromise that I want to draw your attention to.

In order to illustrate the difference between applying Bootstrap styles to custom selectors, and defining purely custom styles, the worked example separates those two tasks into two distinct sections (parts three and four). Not only are they described separately in the text, they also appear separately in the code. This is not realistic!

In reality, that separation adds a layer of complexity to your code that is just not helpful — you should mix-and-match the two approaches within a single set of nested selectors. This will give you the most readable, and hence maintainable code!

To illustrate this real-world approach, here is a snippet from the Sass code powering the Let’s Talk Podcasts site, exactly as it appears in the production code powering the site in June 2026:

/* Style the site header */
header{
  h1 {
    @extend .display-1;
  }
  h2 {
    @extend .bartificer-logo-font;
    font-weight: 100;
    color: map-get($bartificer-colors-light, subheader);
    @extend .fs-4;
  }

  /* Style the nav bar */
  nav.navbar{
    @extend .rounded-1;
    background-color: $bartificer-logo-green;
    @extend .bartificer-subheader-font;

    .navbar-brand{
      @extend .bartificer-header-font;

      img{
        @extend .rounded-1;
        height: 32px;
      }
    }
  }
}

Notice that this snippet both inherits styles into the various elements with @extend, and adds custom style declarations, within the same selectors. Also notice that the code does not just inherit standard Bootstrap classes like .fs-4 for the font-size of <h4> headings, but also inherits custom classes defined higher up the style sheet like .bartificer-header-font.

A second shortcoming of the worked example is that I didn’t succeed in incorporating the realistic use for a Bootstrap function. Ultimately, this is due to the Bartificer Style Guide being so complete — there was simply no reason to derive one colour from another, because the guide provided every needed colour variation!

Had I chosen to use the PBS style as a base, then there might be been a reason to derive a subtle background colour from the PBS logo colour, so we could have needed a variable declaration like:

$logo-background-color: tint-color($logo-color, 10%);

Bootstrap’s tint-color() function takes two arguments: a base colour and a percentage of that base colour to blend into white. If $logo-color was a dark navy blue, then $logo-background-color would become a very light pastel blue.

Final Thoughts

This instalment was intended as taster — to illustrate what is possible and to get you started should you decide this could be a useful approach for some of your future projects. This was by no means a comprehensive guide to costuming Bootstrap with Sass!

However, based on what we did include, these are the key points I want to stress:

  1. Without using Sass, it’s not possible to enable optional Bootstrap features that off by default, or, vice-versa.
  2. Using Sass we can apply Bootstrap styles without needing to add Bootstrap classes to our markup.
  3. By using mixins and Sass inheritance we can apply customised versions of Bootstrap’s styles to any element on our page.
  4. Sass variables and maps facilitate much more customisation than is possible with CSS custom properties:
    1. There are many more Sass variables than there are CSS custom properties
    2. The Sass variables are generally wider-reaching in their effects. For example, when customising the info colour with CSS custom properties in instalment 183, we needed to assign values to multiple variables, but with Sass we only need to assign a single value to the $info variable, and Bootstrap’s Sass code calculates all the variants automatically.
  5. Sass maps make it possible to alter the set of classes Bootstrap generates — removing unwanted variants, and adding new suites for variants. For example, by adding one element to the $spacers map, we generated 84 new spacing classes!

The bottom line is simple — if you want to unitise Bootstrap to it’s full potential, you need to use Sass!

Reminder — PBS 181 Challenge

We’ll finally blow the dust off the challenge set many months ago at the end of the instalment 181 next time. That makes now the perfect time the challenge to help get you back into the Jekyll Mindset.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack