Migrating Bootstrap into an @import layer()
Instead of patching Bootstrap conflicts rule by rule, you can neutralise the whole framework in a single import — file all of Bootstrap into a low-priority vendor layer so every rule you write wins by default. This clean-slate migration is part of Resolving Third-Party CSS Conflicts within Specificity Management & Conflict Resolution.
Prerequisites
Before you begin:
- Understand how layer declaration order makes an earlier-declared layer lose to a later one — the mechanism that demotes
vendorbeneath everything you author. - Know that unlayered CSS behaves as an implicit final layer above all named layers, so Bootstrap loaded the normal way (unwrapped) actively outranks your styles — the problem this page removes at the source.
- This is the clean-slate approach: import Bootstrap into a layer once, from the start, so no conflicts ever arise. It is deliberately different from fixing Bootstrap conflicts using layer overrides, which patches an already-unlayered Bootstrap after conflicts appear. Use this page when you control the import; use the override page when you inherit a project that does not.
- Tooling: Bootstrap 5.x, and — for the build paths — Vite 5+ or webpack 5+ with
css-loader.
Clean slate versus patching: why the import point matters
There are two ways to make your styles win over Bootstrap. The patching approach writes more specific selectors (or !important) each time Bootstrap’s .btn or .card beats you — a growing list of counter-rules that you maintain forever. The clean-slate approach changes where Bootstrap enters the cascade: wrap the entire framework in a vendor layer at import time and it drops beneath every layer you author, so no counter-rules are ever needed.
The difference is structural. Patching manages conflicts; the clean-slate import eliminates the conditions that produce them. Because layer order is evaluated before specificity, a plain .btn in your components layer beats Bootstrap’s .btn.btn-primary in vendor without matching or exceeding its selector weight. You stop thinking about Bootstrap’s specificity entirely — it is simply “the layer below.”
This is the same principle as framework integration and layer wrapping, applied specifically to Bootstrap’s global stylesheet.
Step-by-step procedure
Step 1 — Declare vendor as the lowest layer
Open your entry stylesheet with a single @layer statement that puts vendor first. First-declared means lowest priority, so everything you author afterwards outranks it.
/* entry.css — the first stylesheet the browser parses */
/* WHY: vendor is declared first, so it holds the LOWEST priority.
Every layer after it — reset through overrides — beats Bootstrap
for the same property, with no specificity contest. */
@layer vendor, reset, base, themes, components, utilities, overrides;What this does: Establishes the cascade contract before any rule is parsed. vendor is now the floor of the stack, waiting for Bootstrap to be slotted into it.
Step 2 — Import all of Bootstrap into the vendor layer
Add the layer(vendor) modifier to the Bootstrap @import. Keep it at the top — only @charset and @layer statements may precede a layered @import.
/* entry.css, immediately after the @layer declaration */
/* WHY: layer(vendor) wraps EVERY Bootstrap rule inside @layer vendor
as it loads. This is the clean slate — one line demotes the whole
framework instead of patching its rules one at a time. */
@import url("bootstrap.css") layer(vendor);
/* Bootstrap's own component styles still work internally: all its
rules share the vendor layer, so their mutual order is unchanged. */What this does: Files the complete Bootstrap stylesheet into @layer vendor. Bootstrap keeps styling its own components (its rules still relate to each other normally), but relative to your code the whole framework now sits at the bottom of the priority order.
Step 3 — Wrap Bootstrap in the bundler when using Sass or JS imports
If you import Bootstrap via import "bootstrap/dist/css/bootstrap.css" in JS or compile it from Sass, there is no CSS @import to annotate. Wrap the compiled output in an @layer vendor block instead, so the annotation survives bundling.
/* vendor.css — a module your bundler inlines first */
@layer vendor {
/* WHY: importing INSIDE the block wraps Bootstrap's contents in
@layer vendor even after the bundler flattens the @import.
The annotation rides on the block, not the import statement,
so no runtime request is made and the layer is preserved. */
@import "bootstrap/dist/css/bootstrap.css";
}// vite.config.js — process CSS through PostCSS so the @layer wrapper
// in vendor.css is preserved through bundling.
export default {
css: { transformer: "postcss" },
};
// webpack.config.js (equivalent) — css-loader inlines the @import
// inside @layer vendor; no extra plugin needed.
// module: { rules: [{ test: /\.css$/, use: ["style-loader", "css-loader"] }] }What this does: Produces one bundled stylesheet in which Bootstrap is already wrapped in @layer vendor, with no runtime @import fetch. Whether Bootstrap arrives via Sass compilation or a JS import, the surrounding block guarantees it lands in the vendor tier.
Step 4 — Author your own styles in higher layers
With Bootstrap demoted, write your components normally. They win by layer position, so you never reach for a more specific selector to beat a framework class.
/* entry.css, after the Bootstrap import */
@layer components {
/* WHY: :where() keeps this at zero specificity, yet it still beats
Bootstrap's .btn.btn-primary (specificity 0,2,0) because
components is a LATER layer than vendor. No override needed. */
:where(.btn) {
border-radius: 0;
font-weight: 600;
background: var(--brand, #0057e7);
color: #fff;
}
}What this does: Demonstrates the clean-slate payoff — a zero-specificity rule overrides a higher-specificity Bootstrap rule purely on layer order. Your components layer is authored without any awareness of Bootstrap’s selector weights.
Step 5 — Verify no override rules are required
Confirm the win comes from layer order, not accidental specificity, by deliberately using the weakest possible selector against a strong Bootstrap one.
@layer components {
/* Intentionally weaker than Bootstrap's compound selector.
If this still wins, the clean-slate import is working. */
:where(.btn) { background: teal; }
}
/* Bootstrap in vendor: .btn-primary { background:#0d6efd } (0,1,0)
Your rule: :where(.btn) (0,0,0) — lower specificity, yet it WINS. */What this does: Proves the migration is complete. A (0,0,0) selector beating a (0,1,0) Bootstrap rule can only happen because layer order is checked first — meaning no override rules or !important are needed anywhere in the project.
Verification
Three checks confirm Bootstrap is fully demoted and no patches are hiding.
DevTools layer trace (Chrome): Select a Bootstrap-styled element, open Elements → Styles, and confirm every Bootstrap rule appears under a Layer vendor heading while your winning rule sits under Layer components. Any Bootstrap rule with no Layer heading is still unlayered — see debugging unlayered author styles in DevTools to hunt it down.
Grep for leftover patches: After a clean-slate migration, override hacks should be gone. Search for them and delete any that remain.
# WHY: a successful clean-slate import makes these unnecessary.
# Surviving matches are patches you can now remove.
grep -rnE "!important|\.btn\.btn|#[a-z-]+ \.btn" src/Stylelint guard: Keep the layered import valid and important-free.
{
"rules": {
"no-invalid-position-at-import-rule": true,
"declaration-no-important": true
}
}Troubleshooting
Bootstrap rules appear with no Layer heading in DevTools
: The layer(vendor) annotation was voided because a rule or another @import preceded it. Only @charset and @layer statements may sit above a layered @import. Move the Bootstrap import to the very top of the entry file, directly under the @layer declaration.
Bootstrap loads via JS import and stays unlayered
: A bare import "bootstrap/dist/css/bootstrap.css" in JavaScript produces unlayered CSS. Switch to the Step 3 pattern — a vendor.css module with @layer vendor { @import "bootstrap…"; } — and import that module instead, so the bundler wraps Bootstrap in the layer.
Your component wins in one browser but Bootstrap wins in another
: One build path is layering Bootstrap and another is not. This happens when a dev server inlines @import differently from the production bundle. Verify the compiled output in both environments contains @layer vendor { … } around Bootstrap; align the two build configs so the wrapper is applied identically.
A Bootstrap utility with !important still overrides your component
: Bootstrap ships some utilities with !important (for example spacing helpers). !important inverts layer order, so an important declaration in vendor beats a normal declaration in components. Override that specific property in a later layer, or set your value with !important in utilities — the role of !important in layers covers the inversion. This is the one place clean-slate import cannot help by itself.
Bootstrap’s own components look broken after wrapping
: This means Bootstrap’s rules were split across layers — some in vendor, some unlayered — so their internal order broke. Ensure the entire framework enters through one @import ... layer(vendor) or one @layer vendor block. Bootstrap must be wholly in one layer for its internal cascade to stay intact.
Complete working example
A self-contained entry stylesheet that performs the clean-slate migration and proves no override rules are needed. Copy it as your entry.css.
/* ============================================================
entry.css — load before every other stylesheet
============================================================ */
/* 1. Lock the order: vendor first = lowest priority */
@layer vendor, reset, base, themes, components, utilities, overrides;
/* 2. Clean slate: the ENTIRE framework enters the vendor layer.
For a bundler/Sass build, swap for:
@layer vendor { @import "bootstrap/dist/css/bootstrap.css"; } */
@import url("bootstrap.css") layer(vendor);
/* 3. Foundations of YOUR system */
@layer base {
:root {
--brand: #0057e7;
--font-body: system-ui, -apple-system, sans-serif;
}
body { font-family: var(--font-body); line-height: 1.6; }
}
/* 4. Your components beat Bootstrap by LAYER ORDER, not specificity.
Note every selector here is zero- or one-class weight; none of
them try to out-specify Bootstrap's compound selectors. */
@layer components {
/* Bootstrap's .btn is (0,1,0); this :where(.btn) is (0,0,0)
and still wins because components is a later layer than vendor. */
:where(.btn) {
font-weight: 600;
border-radius: 0;
background: var(--brand);
color: #fff;
border: 0;
padding: 0.5rem 1.25rem;
}
/* Overriding a Bootstrap card with no selector gymnastics */
:where(.card) {
border: 1px solid var(--brand);
box-shadow: none;
}
}
/* 5. Utilities and overrides remain available above components.
Because Bootstrap sits in vendor, these tiers are free for
YOUR helpers — not consumed fighting the framework. */
@layer utilities {
.u-flush { margin: 0; }
}
@layer overrides { /* stays sparse — no Bootstrap patches live here */ }Open DevTools on a <button class="btn btn-primary">: the Styles pane shows Bootstrap’s .btn-primary under Layer vendor, crossed out, and your :where(.btn) under Layer components as the winner — confirming the clean-slate import removed every reason to write an override.
FAQ
How is a clean-slate vendor import different from writing override rules?
A clean-slate import demotes all of Bootstrap into a low-priority vendor layer once, at import time, so every one of your own rules wins by layer order automatically. Override rules instead patch individual conflicts after the fact with more specific selectors or !important. The clean-slate approach removes the conflict class entirely; override rules manage conflicts one at a time.
Does importing Bootstrap into a vendor layer break its own component styles?
No. Bootstrap still styles its own components normally because all of its rules stay in the same vendor layer, so their relative specificity and source order are unchanged. Only the relationship between Bootstrap and your styles changes: your layers now sit above vendor, so your rules win wherever they target the same element.
Can I wrap Bootstrap in a layer when importing it through Sass?
Yes. Sass @use and @import do not emit CSS @import statements, so you wrap the compiled output instead. Put Bootstrap’s Sass entry inside a CSS @layer vendor block, or have the bundler wrap the compiled Bootstrap stylesheet in @layer vendor. The result is a single stylesheet with all Bootstrap rules layered and no runtime request.
Related
- Fixing Bootstrap Conflicts Using Layer Overrides — the patch-based counterpart when Bootstrap is already unlayered
- Framework Integration & Layer Wrapping — the general pattern for wrapping any framework at the build step
- Resolving Third-Party CSS Conflicts — parent section on taming vendor stylesheets
- Specificity Management & Conflict Resolution — root section on selector weight and layer strategy