Sass Customization In Depth

Extending the $theme-colors map, trimming the build to only what you use, the utilities API, and a complete custom-themed example.

Beyond a single variable override

The Utilities & Customization page in this track showed the basic mechanism: install Bootstrap's Sass source via npm, override a variable like $primary, then @import "bootstrap/scss/bootstrap" so every component picks up the change. That covers the simplest case — a single accent color swap. A real production theme usually goes further: adding an entirely new named color that behaves like a first-class Bootstrap color (with its own button, badge, and text/background utility variants generated automatically), trimming the build to only the components actually used, and using Bootstrap's own utilities API to add or remove utility classes.

Extending the $theme-colors map

Bootstrap's colors aren't a handful of separate variables — they're entries in one central Sass map, $theme-colors, and every component that offers color variants (.btn-{color}, .badge.bg-{color}, .text-{color}, alert variants, and more) is generated by looping over that map. Adding a new entry to the map, rather than only overriding an existing one, gets you a brand-new color with the same full set of component variants Bootstrap generates for its built-in colors:

Scss
// custom.scss

@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";

// Merge a new color into the existing map instead of replacing it outright
$theme-colors: map-merge($theme-colors, (
    "brand": #7c3aed,
));

@import "bootstrap/scss/bootstrap";
HTML
<button class="btn btn-brand">Custom color</button>
<span class="badge bg-brand">New</span>
<p class="text-brand">Branded text</p>

map-merge() is important here rather than reassigning $theme-colors directly — merging adds "brand" alongside every existing entry (primary, secondary, success, and so on), while a direct reassignment would replace the whole map and silently delete every built-in color's component variants.

Trimming the build: importing only what you use

The single @import "bootstrap/scss/bootstrap" used so far pulls in every component's Sass, whether or not the project actually uses it. Bootstrap's Sass source is split into many individual partial files specifically so a project can import only what it needs:

Scss
// custom.scss

// Required — every build needs these
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/root";

// Pick only the pieces this project actually uses
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/utilities";

// Explicitly skipped: carousel, offcanvas, tooltips, popovers, toasts, etc.

This produces a noticeably smaller compiled CSS file than importing the full bundle, at the cost of having to remember to add a partial back in later if the project starts using a component it previously skipped (a missing @import for a component whose HTML/JS you've added shows up as completely unstyled markup, which is usually an obvious enough signal to track down).

The Utilities API — adding and removing utilities

Bootstrap 5 generates its entire utility class set (.m-*, .p-*, .text-*, .d-*, and dozens more) from one configurable Sass map, $utilities, rather than as hand-written individual CSS rules. That means new utilities can be added, and unwanted ones removed, the same way $theme-colors was extended above:

Scss
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";

// Add a brand-new utility Bootstrap doesn't ship by default
$utilities: map-merge($utilities, (
    "cursor": (
        property: cursor,
        class: cursor,
        values: auto pointer grab not-allowed,
    ),
));

@import "bootstrap/scss/bootstrap";
HTML
<div class="cursor-pointer">Hover me</div>
<div class="cursor-not-allowed">Disabled-looking cursor</div>

The same map can also disable utilities a project never uses (reducing output size further) by setting an existing key's value to null before the final import — the mechanism is the same one used to extend $theme-colors, applied to a different underlying map.

A complete custom-themed example

Putting the pieces together — overridden core variables, an extended color map, and a themed component — into one custom.scss:

Scss
// custom.scss
@import "bootstrap/scss/functions";

// 1. Core variable overrides, before anything else is imported
$primary:        #4f46e5;
$border-radius:  0.75rem;
$font-family-sans-serif: "Inter", sans-serif;

@import "bootstrap/scss/variables";

// 2. Add a project-specific named color alongside the overridden primary
$theme-colors: map-merge($theme-colors, (
    "brand": #f97316,
));

// 3. Pull in the rest of Bootstrap, which now compiles against every override above
@import "bootstrap/scss/bootstrap";
Bash
sass custom.scss public/css/app.css
HTML
<div class="container py-5">
    <button class="btn btn-primary">Primary action</button>
    <button class="btn btn-brand">Brand accent</button>
    <div class="card mt-4" style="border-radius: var(--bs-border-radius);">
        <div class="card-body">
            Every rounded corner, button color, and font on this page traces back
            to the three variables set at the top of custom.scss.
        </div>
    </div>
</div>

Because $primary, $border-radius, and the new "brand" map entry are all set before Bootstrap's own Sass is imported, every single component that references them — buttons, form focus rings, badges, the card's border radius — picks up the override automatically, with zero component-by-component CSS overrides written after the fact.

Common mistakes

  • Reassigning $theme-colors directly ($theme-colors: ("brand": #f97316);) instead of merging into it — this deletes every built-in color's variants (primary, danger, success, and the rest), since the map now contains only the one new entry.
  • Declaring variable overrides after @import "bootstrap/scss/bootstrap" — Sass variables in this style only affect definitions compiled after them, so an override placed too late is silently ignored by every component.
  • Importing individual component partials (buttons, forms, etc.) without first importing functions, variables, and mixins — most of Bootstrap's component Sass depends on mixins and variables defined in those foundational files, and skipping them causes compile errors.
  • Manually writing a competing set of !important-heavy CSS overrides after the CDN build instead of using the actual Sass variable/map system — it fights specificity forever and has to be redone for every new component, where a Sass-level override applies consistently everywhere at compile time.