Most Bootstrap overrides fail for a boring reason. Your rule loads too early, or it carries less weight than Bootstrap’s selector, and the browser quietly picks the other one.
Which fix applies depends on what you load. With a CDN link or a precompiled file, you can link a custom stylesheet after bootstrap.min.css (with equal or higher selector weight) or reassign the bs-prefixed CSS custom properties at runtime. Sass source adds a third route, setting Sass variables before Bootstrap’s variables file at compile time.
Bootstrap 5.2.0 gave nearly all components CSS variables in July 2022 (Bootstrap blog), so a plain stylesheet can restyle them without a Sass recompile.
One gotcha catches almost everyone. Changing –bs-primary on :root does not recolor .btn-primary. The button-variant() mixin writes literal color values into the –bs-btn-* variables at compile time (Bootstrap 5.3 docs), so the button never reads –bs-primary at all.
Why Does Bootstrap CSS Win Over Custom Styles?
Usually it wins because your selector is lighter, or because your file loads first. A custom rule beats a Bootstrap rule only when its selector is more specific, or equally specific and loaded later.
In practice, overriding means swapping Bootstrap’s defaults for your own declarations, variables or layers. Editing bootstrap.css or anything inside node_modules doesn’t count. The next install wipes the change, and Bootstrap’s own Sass docs tell you to leave the core files alone.
How selector weight is compared
Browsers score a selector in three columns and compare them left to right (MDN, 2026). An ID adds 1-0-0. Classes, attributes and pseudo-classes add 0-1-0 each, and elements and pseudo-elements add 0-0-1.
Columns never carry over. No number of element selectors beats one class.
Combinators add nothing, so .navbar-nav .nav-link scores 0-2-0, the same as .btn.btn-primary. A bare .btn-primary scores 0-1-0, which means a plain .btn-primary rule in your stylesheet only ties it.
When two selectors look close, paste both into a CSS specificity calculator instead of counting by hand.
Why source order breaks ties
Equal weight means the last rule wins.
A custom stylesheet linked before bootstrap.min.css loses every tie. Link the same file after it and it wins them.
Origin and importance get checked before weight, so a layered rule or an !important declaration skips the comparison entirely.
Choosing an Override Method by Build Setup
The build setup decides which methods you can even use. Besides a custom stylesheet and CSS custom properties, you get Sass variables with maps and the utility API, cascade layers, and !important.
| Method | Build step | How it wins | Fails when |
|---|---|---|---|
| Custom stylesheet | No | Equal or higher weight, loaded later | Bootstrap’s selector is heavier, or another stylesheet loads after yours |
| CSS custom properties | No | Reassigns bs-prefixed values on a component | The property has no variable, or the value sits in a media query |
| Sass variables, maps, utility API | Yes | Changes the compiled output at the source | The variable is set after the Bootstrap import |
| Cascade layers | No | Layer order beats weight | A Bootstrap rule carries !important |
| !important | No | Wins within a layer whatever the weight | The next override needs !important too |
On a CDN link, start with a custom stylesheet and add bs-prefixed variables for component tweaks. Nothing to compile. Precompiled dist files get the same two methods, since the CSS is already built.
If you have Sass source through npm, go for variables and maps first. Keep a custom stylesheet only for rules Bootstrap exposes no variable for.
Cascade layers and !important sit at the bottom of the table for a reason. Both are escape hatches, not starting points.
Skip Sass on a single static page that pulls Bootstrap from a CDN. A compiler, an import order and a build script are heavy machinery for a handful of color changes.
Every method below targets the same element, the primary Bootstrap button, so the trade-offs stay comparable.
How to Load a Custom Stylesheet After Bootstrap
Link the custom file after bootstrap.min.css in the head, then write rules that match or beat the weight of the Bootstrap selector.
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="css/custom.css" rel="stylesheet">Bootstrap’s docs pin the exact version in the jsDelivr URL and add an integrity hash. Copy both. A pinned URL serves one fixed file, so a CDN-side update can’t shift the base your overrides sit on.
The obvious first attempt is a direct property rule.
.btn-primary {
background-color: #0f766e;
}That recolors the resting state and nothing else. Hover and active keep Bootstrap’s blue, because those states read their own –bs-btn-hover-bg and –bs-btn-active-bg variables, which the rule never touches.
When a stylesheet override does nothing at all, check the file path first. A wrong path returns a 404 and no rule ever loads, which is the dumbest cause and still the one I look for first.
After that, look for a theme or plugin stylesheet that loads after yours and reverses the order. Or Bootstrap’s selector may simply carry two classes against your one.
A second class is the cheapest way to gain weight, since .btn.btn-primary scores 0-2-0. Reaching for IDs starts a specificity war that every later override has to fight.
Per-state recoloring is where this method gets tedious. The next section handles it with a handful of variables instead of one rule per state.
Overriding Bootstrap With bs-Prefixed CSS Custom Properties
Bootstrap 5 ships its design values as bs-prefixed CSS custom properties inside the compiled CSS, so a plain stylesheet can change them at runtime with no Sass compile.
Component-level variables
Buttons gained local variables on .btn in v5.2.0, and navbars followed in the same release (Bootstrap 5.3 docs). Bootstrap assigns them at the base component level, so you override on that class or on a wrapper around it.
.btn-primary {
--bs-btn-bg: #0f766e;
--bs-btn-border-color: #0f766e;
--bs-btn-hover-bg: #115e59;
--bs-btn-hover-border-color: #115e59;
--bs-btn-active-bg: #134e4a;
--bs-btn-active-border-color: #134e4a;
}This rule has the same weight as Bootstrap’s own .btn-primary and loads later, so it wins the tie. Resting, hover and active states all follow.
For a single element, set the variables inline, the way Bootstrap’s button docs do for custom padding and font size.
Color modes with data-bs-theme
Color modes arrived in Bootstrap 5.3.0 and run on the data-bs-theme attribute. Put it on the html element and it applies page-wide. Put it on any other element or component and it scopes to just that element.
Default light values live under :root and [data-bs-theme=light], with dark overrides under [data-bs-theme=dark]. A custom mode is one more selector, such as [data-bs-theme=”blue”], that reassigns –bs-body-bg, –bs-body-color and any component variables.
Component tweaks inside a mode block work the same way as before. You reassign the –bs-btn-* variables, not the properties.
Where CSS variables stop working
Setting –bs-primary on :root won’t touch .btn-primary.
Bootstrap’s button-variant() mixin writes literal color values into the –bs-btn-* variables at compile time, so the button never reads –bs-primary. Repainting a whole palette from the browser side means reassigning each component’s variables. Recompiling with $primary does it in one change (covered in the Sass section below).
Breakpoints hit a second wall. Bootstrap exposes them as variables, but CSS variables can’t be used inside media queries, so changing a breakpoint stays a Sass job (Bootstrap 5.3 docs).
Not every property has a variable either. Check the component’s CSS section in the docs, and fall back to a stylesheet override when it lists none.
How to Customize Bootstrap With Sass Variables and Maps
Set your variable overrides after Bootstrap’s functions and before its variables file. Every Bootstrap variable carries the !default flag, so a value assigned earlier is never replaced (Bootstrap 5.3 docs).
- Install Bootstrap and the Sass compiler with npm:
npm install bootstrapandnpm install -g sass. The sass package is Dart Sass. - Create scss/custom.scss beside node_modules so Bootstrap’s own files stay untouched.
- Import Bootstrap’s functions file first.
- Add your overrides, such as
$primary: #0f766e;. - Import variables, variables-dark, maps, mixins and root, then the optional parts you need.
- Compile with
sass --watch ./scss/custom.scss ./css/custom.cssand link the output instead of bootstrap.min.css.
@import "../node_modules/bootstrap/scss/functions";
$primary: #0f766e;
$body-bg: #fafaf9;
@import "../node_modules/bootstrap/scss/variables";
@import "../node_modules/bootstrap/scss/variables-dark";
@import "../node_modules/bootstrap/scss/maps";
@import "../node_modules/bootstrap/scss/mixins";
@import "../node_modules/bootstrap/scss/root";
@import "../node_modules/bootstrap/scss/reboot";
@import "../node_modules/bootstrap/scss/type";
// add the components you needEvery overridable variable lives in scss/_variables.scss. Variables set to null output no property until you override them.
A variable placed after the variables import arrives too late for anything Bootstrap has already computed from it, such as the $theme-colors map. Using Webpack, Parcel or Vite instead of the CLI? Bootstrap’s docs carry a compile guide for each.
Extending the $theme-colors map
Changing a color is the easy case. Reassign the standalone variable, such as $primary or $danger, before the variables import.
Adding one takes more work. Merge a new map into $theme-colors with map-merge, and know that a new key alone isn’t enough for alerts and list groups. The color modes docs call for 6 more maps: $theme-colors-text, $theme-colors-bg-subtle and $theme-colors-border-subtle for light mode, plus their -dark counterparts.
Removing a color is a map-remove call after the variables import and before the maps import. Keep primary, success and danger, since Bootstrap uses those keys for links, buttons and form states and removing them can break compilation.
Changing utilities with the Utility API
The utility API generates utility classes from the $utilities map, so you change them in Sass rather than in a competing CSS rule.
Reuse an existing key to override a utility. To add one, use map-merge after importing the utilities file, then import utilities/api last. To drop one, run map-remove($utilities, "width", "float") or set its key to null.
Generated utilities include !important by default, controlled by $enable-important-utilities.
What the Sass @import deprecation means for this setup
As of October 2026, the Sass documentation still lists @import as deprecated since Dart Sass 1.80.0. Removal is not expected before Dart Sass 3.0.0, which ships no sooner than two years after 1.80.0.
Bootstrap 5.3’s docs keep using @import in their examples and say the deprecation warnings can be ignored while a long-term fix is in progress. The Bootstrap 6 alpha docs already use @use and @forward. Stay on @import while you are on Bootstrap 5.3, and move to @use when you upgrade to v6.
To quiet the console, pass --silence-deprecation import to the compiler. Warnings about global built-in functions are a separate deprecation with their own global-builtin flag.
Can Cascade Layers Replace Higher Specificity?
For normal declarations, yes. Once layer order is set, MDN’s @layer reference says specificity and order of appearance stop mattering.
MDN’s specificity guide names Bootstrap as the case for it. Import foreign CSS into a layer instead of fighting it with !important.
@layer bootstrap, overrides;
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css") layer(bootstrap);
@layer overrides {
.nav-link {
color: #0f766e;
}
}A plain .nav-link rule now beats Bootstrap’s .navbar-nav .nav-link at 0-2-0, because the layer comparison happens before any weight is counted.
The first declared layer has the lowest priority and the last has the highest. Declaring a layer name again appends rules without changing the order. Unlayered CSS beats every layered rule, yours included, and the @import must sit above every rule except @charset and @layer statements.
Browser support is no longer a blocker. MDN lists @layer as available across browsers since March 2022.
One quirk to remember is that !important flips the order, so an !important rule inside a layer beats an !important rule outside one.
There’s a side effect on other CSS too. Everything unlayered now outranks Bootstrap, plugin stylesheets included, so check pages that mix libraries.
When Is !important Acceptable in Bootstrap Overrides?
Rarely. It earns a place only when you must beat another !important or an inline style, and every use needs a comment (MDN).
A Bootstrap utility class on an element you can’t touch is one case. Inline styles written by a script, or third-party widget CSS from a source you can’t reorder or edit, make up the rest.
Bootstrap’s utility API docs explain the first one. Utilities carry !important so they override components and modifier classes as intended, which means your component rule loses to a .bg-* or .text-* class on the same element, however heavy its selector is.
Inline styles can only be beaten with !important, according to MDN, which also suggests a narrow attribute selector to keep the damage small.
/* Undoes an inline height set by a script. Remove when the script is fixed. */
.vendor-widget[style*="height"] {
height: auto !important;
}Keep the exceptions in one place. MDN’s pattern is a dedicated cascade layer, declared first, that holds only the important overrides.
The upside is real. It wins within a layer whatever the selector weight, and it’s the only tool that beats an inline style or another !important. No build step either, so it works on CDN-linked Bootstrap.
The cost shows up later. The next override has to use !important too, which is how the pile grows. It also reverses layer order, so it fights any cascade layers setup, and MDN calls it a bad practice when used to override specificity.
Layout and component base styles never qualify. If a base style needs !important to win, a bs-prefixed variable or a layer is missing from the setup.
How to Find the Winning Rule in DevTools
Select the element, read the Styles pane for struck-through declarations, then open the Computed tab and follow the property back to the rule that sets it.
- Right-click the element and choose Inspect, or press Ctrl+Shift+C (Cmd+Shift+C on Mac) and click it on the page.
- In the Styles pane, find the property. Overridden declarations appear struck through, and hovering a selector in Chrome shows its specificity weight.
- Force the state you are debugging. Chrome’s :hov button and Firefox’s pseudo-class button both toggle :hover, :active and :focus on the selected element.
- Open the Computed tab for the final value. Firefox lets you expand a property to see the rule that set it, with a filename and line number.
- Click the source link beside the rule to confirm which stylesheet it came from and in what order the files loaded.
- Test the fix in place: tick a declaration off, edit a value, or add a class. Edits are temporary, so copy the working rule into your stylesheet.
Firefox has a funnel icon next to an overridden declaration that filters the Rules view to every rule setting the same property. That is the full cascade for it (Firefox Source Docs).
In Chrome, hovering a custom property shows its value, and var() calls link to the declaration they read. It’s the quickest way to trace a bs-prefixed variable such as –bs-btn-bg to its source.
If Bootstrap sits in a cascade layer, Chrome groups rules by layer, and clicking a layer name shows the layer order.
When none of your rules appear at all, the stylesheet never loaded. A failed to load resource error in the Console usually points to a wrong path.
The minified bootstrap.min.css is hard to scan by eye, so run it through a CSS beautifier before searching it for a selector.
What Breaks Bootstrap Overrides Over Time
Overrides rot in a handful of predictable ways: specificity creep, upgrades that change component internals, unpinned CDN URLs, edits inside node_modules, and the coming Sass @import removal.
| Failure | Trigger | First fix |
|---|---|---|
| Specificity war | Each override adds IDs or extra classes | Lower weight with :where() |
| Upgrade change | Bootstrap rewrites component internals | Diff the component’s variables |
| Unpinned CDN URL | Version omitted or @latest | Use an exact version |
| Edit in node_modules | A clean install wipes the folder | Move the change into your own file |
| Sass @import removal | Sass starts erroring on @import | Move to @use, which Bootstrap 6 already uses, when you upgrade |
Specificity wars
Every ID you add forces the next override to add another.
MDN advises keeping specificity as low as possible, and says its duplicate-selector trick (.a.a.a) belongs in sparing, commented use at most.
A cleaner exit is :where(), which always scores 0-0-0. Writing :where(#hero) .btn-primary keeps the context but drops the ID’s 1-0-0 weight, so later rules tie again and source order decides.
Upgrades that change component internals
Bootstrap 5.2.0, published July 19, 2022, refreshed buttons and inputs with new padding and border-radius (Bootstrap blog). Nearly all components gained bs-prefixed CSS variables in that release.
Thicker table group dividers became opt-in through .table-group-divider. And 8 maps moved into a new _maps.scss, including $theme-colors-rgb and $gutters, so custom builds needed a separate maps import.
Any override that hard-coded button padding or rewrote Bootstrap navbar styles needed rechecking after that release.
Bootstrap’s docs present the variables as the intended customization surface, so a variable reassignment depends on a documented name instead of an internal property rule. Names can still change between releases, so diff the component’s CSS section after each upgrade.
CDN and install drift
Leaving the version off or using @latest is something jsDelivr advises against in production, because major releases usually bring breaking changes (jsDelivr documentation). A range such as @5 follows every new release inside that major, component changes included.
Edits in node_modules last only until the next clean install, since npm ci removes an existing node_modules folder before it installs (npm docs, v10).
Keep every change in custom.css or custom.scss, where version control tracks it.
Sass @import removal
Sass’s migration notes describe where the deprecation ends. Right now @import produces deprecation warnings, and by default in terse mode Sass prints each deprecation type only five times (--verbose shows all of them).
Later, likely, comes a period where @import throws an error. Finally, all @import rules get treated as plain CSS imports.
Bootstrap 5.3’s docs still show @import and call the warnings ignorable, so a pipeline copied from them has an expiry date. The Bootstrap 6 alpha docs use @use and @forward instead. A committed package-lock.json keeps the Sass version frozen between clean installs, because npm ci never rewrites the lock files.
When a custom build beats stacked overrides
Stacked overrides mean shipping all of Bootstrap with your corrections layered on top, while a Sass build from selected partials gives you a single stylesheet.
Bootstrap’s optimization docs tell Sass users to import only the components they use, and removing the carousel import is their example of saving compiled file size. They also warn that dependencies between partials can make omitting a file harder than it looks.
Switch to a custom build when most rules in your stylesheet just restate Bootstrap selectors, or when you need !important to beat your own earlier rules. Needing per-state rules on every component is another sign.
Until then, a small custom stylesheet is the cheaper maintenance bill.
Overriding Bootstrap CSS FAQ
Can I use a Bootswatch theme instead of overriding Bootstrap CSS?
Yes, when a ready-made look is close to your design. Bootswatch offers 26 free themes as drop-in replacements for bootstrap.css, built for Bootstrap 5.3.8 according to its own site.
Each theme keeps its changes in two Sass files, so a small custom stylesheet can still sit on top.
How do I override Bootstrap navbar and modal styles?
Both components expose local CSS variables. Reassign –bs-navbar-color on .navbar, or –bs-modal-bg on .modal.
For a dark navbar, Bootstrap deprecated .navbar-dark in favor of data-bs-theme=”dark” on the navbar itself.
How do I override Bootstrap form control styles?
Form controls expose no component-level CSS variables, only Sass variables such as $input-border-color and $input-padding-y.
Many of them compile to root variables, so reassigning –bs-border-color or –bs-body-bg changes every Bootstrap form field at runtime. For a change scoped to inputs, write a .form-control rule in the custom stylesheet.
How do I change the container width or grid breakpoints in Bootstrap?
Both live in Sass maps in _variables.scss, called $container-max-widths and $grid-breakpoints. Containers default to 540px through 1320px, and breakpoints to 576px through 1400px, from sm to xxl.
Each map must ascend, and $grid-breakpoints must start at 0. Bootstrap asserts both at compile time.
How do I change the font family in Bootstrap?
Reassign –bs-body-font-family on :root for a runtime change, or set $font-family-sans-serif in your Sass overrides. The Sass value feeds –bs-font-sans-serif, which the body variable reads by default.
Headings inherit the body font, because $headings-font-family defaults to null.
How do I enqueue custom CSS after Bootstrap in WordPress?
Pass Bootstrap’s handle in the $deps array of wp_enqueue_style(), and WordPress prints your stylesheet after it. Hook the call to wp_enqueue_scripts and use the handle your theme registered for Bootstrap.
The handle has to exist first, so register Bootstrap before your stylesheet when adding Bootstrap to WordPress.
Cleaning Up an Override-Heavy Bootstrap Stylesheet
Overriding Bootstrap CSS cleanly means retiring old rules in a set order: !important first, property rules second, palette changes last.
Strip !important wherever weight or load order caused the fight. Swap property rules for bs-prefixed variable reassignments, then move palette changes into Sass variables.
Each step can be checked in DevTools before the next begins. The Sass step goes last because it alone adds a build step.
Bootstrap 6.0.0-alpha.1 was released on October 8, 2026, and the Bootstrap blog’s Docs links now point to the 6.0 docs. It is an alpha, and Bootstrap 5 stays available, with 5.3.8 as the latest stable release. Version 6 moves Sass customization to @use token maps, drops the bs- prefix in its source (it is added back through PostCSS), and raises the browser floor to Chrome and Edge 130, Firefox 132 and Safari 18. A major upgrade would send every surviving override back through an audit against the 6.0 docs.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026

