Framework Integration: Wrapping Tailwind & Bootstrap in @layer

The day you add Bootstrap or Tailwind to a design system built on named layers, the framework’s stylesheet arrives as unlayered author CSS — and unlayered author styles beat every named layer you declared. Your carefully ordered components layer suddenly loses to a framework .btn it should override, and the reflex is another round of !important. This page, part of Architecture Patterns & Design System Scaling, shows how to slot each framework into the correct layer so its rules obey your cascade contract instead of overrunning it — covering Tailwind v4’s native layers, Bootstrap wrapping, and the ordering decisions that keep both predictable.


How frameworks enter the six-layer cascade stack A vertical stack of the canonical layers vendor, reset, base, themes, components, utilities, overrides. Bootstrap flows into the vendor layer at the bottom. Tailwind's preflight flows into base and its utility classes flow into the utilities layer near the top. Your own component rules sit in the components layer between them. Frameworks entering the canonical layer stack (bottom = lowest priority) @layer vendor Bootstrap, legacy UI kits @layer reset @layer base Tailwind preflight @layer themes @layer components your design system @layer utilities Tailwind utilities @layer overrides Bootstrap layer(vendor) Tailwind v4 native @layer higher priority

Concept & spec reference

The CSS Cascading and Inheritance specification places @layer order above selector specificity in the cascade. When two rules match the same element, the browser compares layer position first; specificity is only consulted when both rules live in the same layer. Framework integration is simply the application of that rule to code you did not write: if a framework’s stylesheet is assigned to a layer below your own, its high-specificity selectors can no longer beat your low-specificity ones.

There are two mechanisms for assigning a framework to a layer, and which you use depends on whether the framework emits its own @layer blocks:

/* Mechanism A — wrap a non-layered framework at import time.
   Every rule inside bootstrap.css is placed into @layer vendor. */
@import url("bootstrap.css") layer(vendor);

/* Mechanism B — a framework that already emits @layer blocks
   (Tailwind v4) is re-slotted by nesting its import under a
   wrapper layer so its internal names don't collide with yours. */
@layer vendor {
  @import "tailwindcss";
}

The canonical stack this site uses gains one framework-facing layer at the bottom:

/* vendor sits first so any framework assigned to it loses to everything above */
@layer vendor, reset, base, themes, components, utilities, overrides;

The layer() function on @import is the load-bearing detail. A plain @import url("bootstrap.css") brings Bootstrap in as unlayered author CSS, which sits above every named layer — the exact inversion of what you want. This is the same trap covered in depth under resolving third-party CSS conflicts.

How the browser resolves a wrapped framework

Walk through what happens when Bootstrap’s .btn and your design system’s .btn both match a <button class="btn">.

Step 1 — Layer registration. The browser reads @layer vendor, reset, base, themes, components, utilities, overrides; and registers seven layers in that order. vendor is index 0 (lowest priority).

Step 2 — Rule assignment. The @import url("bootstrap.css") layer(vendor) statement places Bootstrap’s .btn { ... } — a selector Bootstrap ships at specificity (0,1,0) — into vendor. Your @layer components { .btn { ... } } lands in components, index 4.

Step 3 — Cascade resolution. Both .btn rules match. The browser compares layers before specificity: components (index 4) outranks vendor (index 0), so your rule wins. Bootstrap’s identical specificity is never even compared, because the layer comparison already decided the outcome.

@layer vendor, reset, base, themes, components, utilities, overrides;

/* Bootstrap's .btn is now in vendor at (0,1,0) */
@import url("bootstrap.css") layer(vendor);

@layer components {
  /* Wins over Bootstrap's .btn purely by layer order.
     :where() keeps specificity at 0-0-0 so utilities and overrides
     can still adjust this button without a specificity fight. */
  :where(.btn) {
    border-radius: var(--radius-md);
    font-family: var(--font-body);
    background: var(--color-primary);
  }
}

The mental model is worth stating plainly: once a framework is in a lower layer, its specificity stops mattering relative to yours. A framework can ship #root .navbar .btn.btn-primary at specificity (1,3,0) and it will still lose to your :where(.btn) at (0,0,0), because they are compared by layer first. This is exactly the guarantee that makes the base vs utility layer strategies decision tractable when a framework is in the mix.

Practical usage patterns

Pattern 1 — Tailwind v4 native @layer

Tailwind CSS v4 is layer-native. The single @import "tailwindcss" directive expands, roughly, to this:

/* What @import "tailwindcss" emits under the hood in v4 */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);      /* design tokens */
@import "tailwindcss/preflight.css" layer(base);   /* the reset */
@import "tailwindcss/utilities.css" layer(utilities);

Because Tailwind declares its own theme, base, components, and utilities layers, importing it unwrapped would interleave Tailwind’s layer names with yours in first-seen order — non-deterministic if your entry file and Tailwind disagree on ordering. The clean integration nests Tailwind’s whole output under a single wrapper layer you control:

/* Your canonical stack, with a dedicated slot for Tailwind's sub-layers */
@layer reset, base, themes, tw, components, utilities, overrides;

@layer tw {
  /* Tailwind's internal theme/base/components/utilities become
     tw.theme, tw.base, etc. — namespaced so they can never
     reorder or shadow your top-level layers. */
  @import "tailwindcss";
}

If you specifically want Tailwind’s utility classes to override your components (the usual reason teams reach for Tailwind), assign Tailwind’s utilities to your top-level utilities layer instead of burying them in tw. The wrapping Tailwind and Bootstrap in cascade layers walkthrough gives the exact per-step configuration for both choices.

Pattern 2 — Bootstrap via @import layer(vendor)

Bootstrap 5 predates native cascade layers, so it emits ordinary unlayered rules. Wrap the whole distribution at import time:

@layer vendor, reset, base, themes, components, utilities, overrides;

/* The entire Bootstrap bundle drops into vendor with zero edits to
   Bootstrap's own files. Its reset, grid, and components all land
   in the lowest-priority layer together. */
@import url("bootstrap.css") layer(vendor);

@layer components {
  /* Your button overrides Bootstrap's without !important */
  :where(.btn) { border-radius: 0; }
}

If you only need Bootstrap’s components and want its reset to live in your reset layer instead, split the imports by file — assign bootstrap-reboot.css to reset and the component bundle to vendor:

/* Bootstrap's Reboot goes into your reset layer so it participates
   as a baseline, not an opaque vendor artefact. */
@import url("bootstrap-reboot.css") layer(reset);
@import url("bootstrap-grid.css") layer(vendor);
@import url("bootstrap-utilities.css") layer(utilities);

Pattern 3 — The UI-kit sandwich

When you combine a component framework, your own components, and a utility framework, order them as a sandwich: framework resets and components on the bottom bun, your design system in the filling, utility classes and a sparse overrides layer on top.

/* The sandwich: vendor components lose to yours; utilities win over
   yours by intent; overrides is the governed escape hatch on top. */
@layer vendor, reset, base, themes, components, utilities, overrides;

@import url("bootstrap.css") layer(vendor);        /* bottom bun */

@layer components {
  :where(.btn) { background: var(--color-primary); }  /* filling */
}

@layer utilities {
  /* Tailwind utilities (or your own) intentionally beat components,
     so .p-0 flattens a Bootstrap-or-yours button on demand. */
  .p-0 { padding: 0; }
}

The sandwich is why a Tailwind utility can adjust spacing on a Bootstrap-derived component while your own component rules still override Bootstrap’s defaults — each collision resolves by declared layer position, never by which stylesheet the bundler happened to emit last.

Interaction with adjacent features

Framework wrapping does not stand alone; it leans on two neighbouring concepts.

Base vs utility layer strategies decides where a utility framework sits. Tailwind’s utilities above components means a utility class wins over a component default — the standard Tailwind expectation. Utilities below components means components win and utilities become suggestions. The framework you wrap does not change this axis; it only adds a vendor floor beneath it.

Resolving third-party CSS conflicts is the specificity-side view of the same problem. Wrapping a framework in vendor is the structural fix; that guide covers the residual cases — !important inside a framework, inline styles a framework injects via JavaScript, and Shadow DOM widgets a layer cannot reach.

One caution the interaction surfaces: !important inverts layer order. A framework rule marked !important in vendor beats a normal rule in components, and even beats an !important rule in a higher layer. Bootstrap’s .d-none { display: none !important } is the classic example — it keeps working precisely because of this inversion, which is intentional. When a framework’s !important fights you, the fix is an !important declaration in a lower layer than the framework, not a higher one.

DevTools & Stylelint diagnostic workflow

Confirming layer assignment in Chrome DevTools

  1. Open DevTools → Elements → Styles.
  2. Select an element the framework styles (a .btn, a .navbar).
  3. Chrome groups matched rules by layer. Confirm Bootstrap’s rules appear under @layer vendor (or @layer tw for a wrapped Tailwind), not under (unlayered).
  4. If any framework rule shows as (unlayered), its @import is missing the layer() annotation — or a rule appears above the @import, which voids the annotation.
  5. Open Sources → Cascade layers (Chrome 107+) for the full layer tree, and verify vendor is first in the list.

Catching unlayered framework imports with Stylelint

The most common regression is a teammate adding @import 'some-framework.css' without layer(). Enforce it in CI:

// stylelint.config.js
export default {
  plugins: ["@csstools/stylelint-plugin-cascade-layers"],
  rules: {
    // Flags any selector authored outside a named @layer — including
    // framework rules pulled in by a bare @import without layer().
    "@csstools/cascade-layers/require-defined-layers": [
      true,
      { layerOrder: ["vendor", "reset", "base", "themes", "components", "utilities", "overrides"] }
    ]
  }
};

Pair it with a build-time guard that greps for un-annotated framework imports before they reach production:

# WHY: any @import without a layer() annotation lands as unlayered
# author CSS and silently beats the whole stack. Fail the build on it.
grep -rnE "@import +url\([^)]*\); *$" src/styles && exit 1 || echo "all imports layered"

Migration checklist

Follow these steps to move a framework-based stylesheet onto the layer stack:

  1. Declare the canonical layer order. Add @layer vendor, reset, base, themes, components, utilities, overrides; as the first statement in your entry stylesheet, above every framework import.
  2. Map each framework’s cascade footprint. For every framework, note which rules are reset/preflight, which are components, and which are utilities — each maps to a different target layer.
  3. Wrap non-layered frameworks with @import layer(). Replace @import 'bootstrap.css' with @import url('bootstrap.css') layer(vendor) so its rules stop arriving unlayered.
  4. Re-map native-layer frameworks. For Tailwind v4, nest @import "tailwindcss" inside a wrapper layer, or route its utilities to your top-level utilities layer, so its layer names cannot collide with yours.
  5. Delete !important escapes. Every !important that existed only to beat a framework selector can go once the framework sits in a lower layer.
  6. Verify in DevTools and CI. Confirm each framework rule sits under its intended layer in the Styles panel, and run the cascade-layers Stylelint plugin so no unlayered framework import can regress.

Edge cases & gotchas

@import order voids layer()

The layer() annotation on @import only works if the @import appears before any style rule (a @charset or bare @layer declaration may precede it). Put a single .foo {} rule above your framework imports and the browser silently ignores layer(), dropping the framework back into unlayered CSS. Keep every @import at the very top of the entry file.

Bundlers that inline @import

Vite, webpack, and PostCSS’s postcss-import resolve and inline @import at build time. Most preserve the layer() annotation by wrapping the inlined content in an @layer block — but older postcss-import versions strip it. Verify the built artifact contains @layer vendor { ... } around the framework’s rules, not a bare inlined dump. If the annotation is lost, wrap the import manually: @layer vendor { @import "bootstrap.css"; }.

Tailwind v3 is not layer-native

Only Tailwind v4 emits real @layer blocks. Tailwind v3’s @tailwind base; @tailwind components; @tailwind utilities; directives produce unlayered CSS (its @layer at-rule there is a Tailwind-specific build construct, not the CSS cascade layer). A v3 project must be wrapped like Bootstrap — @import url("tailwind-built.css") layer(vendor) — or upgraded to v4 to get native integration.

Framework !important still bites

Bootstrap’s utility classes (.d-none, .text-center) ship !important. Because !important inverts layer order, those declarations beat your normal rules in higher layers by design. If you must override a framework !important, place your !important rule in a layer earlier than the framework’s — counterintuitive, but it is how the inversion resolves.

JavaScript-injected framework styles

Some widgets (date pickers, rich-text editors) inject a <style> tag at runtime. Those styles are unlayered and beat your whole stack. A cascade layer cannot reach them; scope them with a wrapper element and higher-specificity overrides in overrides, or load the widget’s CSS yourself via @import ... layer(vendor) and suppress its runtime injection.

FAQ

Does Tailwind CSS v4 use native cascade layers?

Yes. In v4 the single @import "tailwindcss" expands to a @layer theme, base, components, utilities; declaration plus layered sub-imports, so Tailwind’s own output already lives inside named layers. You integrate by re-slotting those layer names inside your own stack — for example nesting them under a tw wrapper layer — rather than importing Tailwind as unlayered CSS. Tailwind v3 predates native layers and must be wrapped with @import layer() or a build-time wrapper instead.

How do I stop Bootstrap from overriding my design-system components?

Import Bootstrap into a low-priority layer with @import url('bootstrap.css') layer(vendor) and declare vendor before your components layer in the canonical @layer vendor, reset, base, themes, components, utilities, overrides; statement. Because layer order beats specificity, any rule in components wins over Bootstrap’s .btn even though Bootstrap uses higher-specificity selectors — with no !important and no edits to Bootstrap’s files. See resolving third-party CSS conflicts for the residual !important cases.

Should a framework go above or below my component layer?

A full component framework like Bootstrap goes below your components layer so your own components win. A utility framework like Tailwind usually goes above components so utility classes can intentionally override component defaults. That is the UI-kit sandwich: framework base and reset at the bottom, your components in the middle, utility classes on top, and a sparse overrides layer above everything. The base vs utility layer strategies guide covers the trade-off in full.

Can I mix Tailwind utilities and Bootstrap components in the same project?

Yes, and cascade layers are what make it safe. Put Bootstrap in a vendor layer at the bottom, your components in components, and Tailwind’s utilities layer above components. The layer order guarantees a Tailwind utility can adjust spacing on a Bootstrap-derived component while your own component rules still override Bootstrap’s defaults — every collision resolves by declared layer position rather than by whichever stylesheet loaded last.


Up: Architecture Patterns & Design System Scaling