Getting Started with Bootstrap 5: Shipped Bytes, Build Step, and the Class of Customisation It Makes Hard

What Bootstrap 5 ships in bytes after compression, the build step it requires, and which customisation paths its architecture closes off.

Bootstrap 5 arrives with a reputation for weight it does not entirely deserve. The minified CSS is 197 KB uncompressed. Gzip compresses that to 22.1 KB, and Brotli, which most modern servers prefer, shrinks it further to about 17 KB. The JavaScript bundle, with Popper.js v2 baked in, lands at 12.8 KB compressed. Those are the shipped bytes for the 5.3.3 release, the long-term stable line. They change what you should worry about. The real cost is not the wire. It is the build step you must run, the specificity baseline you must beat, and the customisation paths the architecture silently closes. Before you import anything, know what you are signing up for: a Sass compilation step, a utility API that only generates what you ask for, and a cascade that will fight you if you do not use @layer.

Shipped Bytes and Compression

Start with the actual shipped bytes. That is what the CDN serves and what your performance budget measures. The file at bootstrap.min.css is 197 KB uncompressed and 22.1 KB compressed with gzip. Brotli brings it to about 17 KB. The JavaScript bundle at bootstrap.bundle.min.js is 12.8 KB compressed and includes Popper.js v2. You never need a separate Popper import if you use the bundle. The Bootstrap team publishes exact sizes on their GitHub releases page. Verify the integrity hash, a sha256 for the CSS, from the official docs before pinning it in your build.

Compression exploits repetition. Bootstrap's utility classes repeat constantly, which is why 197 KB of source collapses to 22.1 KB. That does not make it free. It makes the network cost acceptable for many projects. The bottleneck shifts from bandwidth to parsing and rendering.

Here is the minimal import that gets you the full CSS framework:

/* ===== styles.css ===== */
@import "../node_modules/bootstrap/scss/bootstrap";

That single line pulls in the entire compiled framework when you do not customise anything. Run it through Dart Sass. LibSass was dropped. You get the full output. To reduce it, stop using the monolithic import and switch to individual files or the utility API. The cost of the naive path is concrete: every component, every utility, every colour shade ships whether you use it or not. Import only what you need. Sass cannot tree-shake the way JavaScript bundlers do. It includes only the partials you explicitly @use or @import. The build step is where you decide what survives.

What the Compressed Size Means for Shipping

Do not confuse the raw byte count with the cost to your users. The 22.1 KB compressed figure travels over the wire. The browser must still parse 197 KB of CSS. That parsing happens on the main thread before first paint. Measure with both gzip and Brotli. Brotli's better compression saves about 5 KB on this file.

If you ship the full CSS-only bundle, accept that the parse cost is fixed. To move the needle, reduce the source. The utility API and component imports are not optional. The Bootstrap docs state the file sizes explicitly. Reproduce them by building from source and running a compression tool. The number that matters is not the compressed figure alone. It is that figure plus parse time. Parse time scales with the uncompressed size.

Utility API Customisation

The utility API is where Bootstrap 5 gets its power and its trap. Configure it in Sass by passing a map to the `utilities` key. It generates only the utilities you request. To add a custom utility for `text-shadow`, write:

// ===== custom-bootstrap.scss =====
@use "bootstrap/scss/bootstrap" with (
  $utilities: (
    "text-shadow": (
      property: text-shadow,
      values: (
        none: none,
        sm: 0 1px 2px rgba(0,0,0,0.5),
        lg: 0 3px 6px rgba(0,0,0,0.5),
      )
    )
  )
);

That works because you are adding to the map. The failure case is trying to replace an existing utility by re-declaring its key without merging. If you write `$utilities: map-remove($utilities, "text-shadow")` you must include the full default map first. Otherwise you silently drop every utility class in the framework.

Worse: adding a utility that depends on a Sass variable the framework does not expose. The build succeeds. The class is missing from the output. No error, no warning. A `.text-shadow-sm` that does not exist in the generated CSS. Any customisation that requires a utility you invented must be declared in the API map. Test the output. The API does not validate your intent.

CSS Custom Properties Migration

Bootstrap 5 ships around 1,400 CSS custom properties in `:root`. They cover colours, spacing, typography, borders, and shadows. These are not a theme system. They are compiled values from the Sass maps, frozen at build time.

Override `--bs-primary` on `:root` to change the primary colour. That works for any component that uses the variable. Here is the migration trap: many component classes do not use the custom property directly. They use the Sass variable that was compiled into the CSS. Overriding `--bs-primary` on a card's parent will not change the card's border colour if the card's rule sets `border-color: var(--bs-card-border-color)`, which itself inherits from `--bs-primary`. The chain works only if every link reads the custom property at the point of use.

Bootstrap's custom properties are a convenience layer, not a theming engine. To change a value at runtime, override the exact variable the component reads. Do it on the element that matches the selector. There is no `@supports` guard. Older browsers without custom property support get nothing. Use the custom properties for what they are: build-time tokens exposed for minor tweaks. Use Sass variables for anything you need to change before compilation.

Tree-Shaking and the Build Step

Tree-shaking in the JavaScript sense does not apply to Bootstrap's CSS. CSS has no module graph a bundler can analyse statically. What you actually pay is the build step cost. You must run Dart Sass. You must configure which SCSS partials to include.

Import `bootstrap/scss/bootstrap` for the full framework. Import individual components like `bootstrap/scss/forms/form-control` to trim it. The cost is not just the file size. It is the compilation time, the custom build script you must maintain, and the discipline to update the import list when you add a component.

The failure mode: forgetting to import `bootstrap/scss/utilities` after importing a component that uses utility classes. The layout breaks silently. This maintenance burden is real. It is not a one-time setup. On a buildless project, you cannot do this at all. The CSS-only bundle is your only realistic option.

The Cascade Layer Pattern

The specificity baseline for Bootstrap is 0,1,0 for most component classes. The framework uses `!important` in a few places. Utilities often carry higher specificity because they are single-class selectors. To take control, use `@layer`:

/* ===== main.css ===== */
@layer reset, base, framework, utilities, components;

@import "bootstrap/dist/css/bootstrap.min.css" layer(framework);

@layer components {
  .card {
    --bs-card-border-color: red; /* overrides Bootstrap's value */
  }
}

The `@layer` statement must come before any `@import` that injects styles into a layer. The order in the declaration sets the priority: later layers win over earlier ones. In the example, `components` wins over `framework`. Your override does not need a higher-specificity selector.

This works in every browser that supports `@layer`. That is Baseline: Chrome 99+, Firefox 97+, Safari 15.4+. Check caniuse for the current support picture. The common mistake is placing the `@import` before the `@layer` declaration. That puts Bootstrap outside any layer. Unlayered styles always beat layered ones regardless of order. Forgetting the `@layer` declaration entirely, or putting it after the import, means Bootstrap wins because it is unlayered. You are back to specificity wars.

The JavaScript Dependency

Bootstrap's interactive components, dropdowns, tooltips, popovers, modals, offcanvas, require JavaScript. The full bundle includes Popper.js v2 as `@popperjs/core`. You get everything in one file. The partial bundle, `bootstrap.min.js`, does not include Popper.

Use a dropdown without loading `@popperjs/core` and you get a silent failure. The dropdown opens and closes without positioning. The docs will not tell you why. The common mistake is loading the partial bundle for a modal and expecting a tooltip to work.

The rule: if you use dropdowns, tooltips, or popovers, load the full bundle or import the Popper dependency yourself. The full bundle is 12.8 KB compressed. The partial is around 8 KB. You save about 4.8 KB by skipping Popper. You lose positional accuracy. For a page where a tooltip is a nice-to-have, the saving is not worth the risk of a broken interaction.

Do not load the bundle on every page when no interactive component is present. Load it only on pages that need it. Use deferred loading with `defer` or `type="module"`.

Specificity and Maintenance

Every override you write must beat Bootstrap's default specificity of 0,1,0. A single class selector like `.btn` on your side loses to Bootstrap's `.btn`. Raise yours to 0,2,0 with `.card .btn`, or use `@layer`. The maintenance cost is real. You will write selectors longer than they need to be. You will hesitate before adding a new component because the override might break.

Set the custom properties on `:root` for the values you want globally. That works only for the variables Bootstrap exposes. For anything else, the cascade is your enemy. If you need to override more than a handful of components, Bootstrap is the wrong tool. The utility API exists to generate your own classes. That requires a build step and discipline. If you cannot commit to that, use a different framework or write your own CSS.

When Bootstrap 5 Fails

Do not use Bootstrap 5 if you need runtime theming, changing the primary colour based on user preference or a server response. The custom properties are frozen at build time. You can override them, but you must know the exact variable name. The override must be on an element that matches the component's selector.

Do not use it if your audience is on devices where every kilobyte counts. Low-end Android phones on 2G connections cannot justify 22.1 KB of CSS plus parse time when a 5 KB custom stylesheet would do.

Do not use it if you need to support Internet Explorer. Bootstrap 5 dropped IE entirely. No polyfills, no fallbacks. The `@layer` pattern requires a modern browser.

Do not use it if your project has a strict design system with a specific token set. The framework's opinionated defaults will fight you. The utility API is a workaround, not a solution. The failure case is a developer who spends two weeks overriding Bootstrap and ends up with a stylesheet larger and harder to maintain than starting from scratch.

FAQ

What is the exact compressed size of Bootstrap 5's CSS? ~22.1 KB for the minified CSS (bootstrap.min.css, 5.3.3). Brotli brings it to ~17 KB.

Do I need to run a Sass build step? No. Use the precompiled CDN CSS. You cannot customise the utility API or reduce the bundle without the build step. It is required only for customisation.

Can I override Bootstrap styles without CSS custom properties? Yes, via `@layer` or higher specificity. Custom properties are the cleanest way to change themable values like colours and spacing.

What is the JavaScript requirement? Interactive components need the bundle. The full bundle includes Popper.js v2. The partial does not. Load the full bundle or Popper separately for dropdowns, tooltips, and popovers.

Is Bootstrap 5 compatible with Internet Explorer? No. IE support was dropped entirely. No polyfills are provided.

What Ships, What It Costs, What You Must Do

ItemValue (5.3.3, minified)What You Must Do
CSS file, uncompressed~197 KBParse cost is fixed if you use the full bundle
CSS file, compressed (gzip)~22.1 KBNetwork cost is acceptable for most projects
CSS file, Brotli~17 KBUse Brotli if your CDN supports it
JS bundle (full, with Popper)~12.8 KB compressedLoad only if you have interactive components
JS bundle (partial)~8 KB compressedRequires separate Popper for positioning
CSS custom properties in `:root`~1,400Override on `:root` or the component's parent
Sass build stepRequired for customisationUse Dart Sass; LibSass is dead
IE supportNoneDo not use for IE audiences

The 1 AM Override Failure

You have spent an hour trying to override `.btn-primary`. Nothing changes. Check the cascade. Is your stylesheet loaded after Bootstrap's? Is the selector specificity equal? Add `!important` to the property as a diagnostic, then remove it. That is a dead end.

The real fix: find the exact custom property. `--bs-btn-bg` for a button. Override it on the button or its parent. If that does not work, the override is in the wrong layer. Wrap your rule in `@layer components`. Ensure the layer declaration appears before the import. If it still fails, you have hit a `!important` inside Bootstrap. It is rare but it exists.

The nuclear option: use the utility API to generate a new class. Or drop the override and use a different component. Do not fight the framework. The page you are building at 1 AM will not reward stubbornness.

Cutting the Last Kilobyte

If 22.1 KB compressed is still too much, Bootstrap is not the problem. Your design is. Keep the utilities you use. The utility API generates only what you need. Drop the components you do not. A page using only layout utilities and a button can ship a 5 KB file. Load a modal or a tooltip, and you are back to the full CSS.

Every kilobyte you save in CSS is a kilobyte you pay in custom code you must maintain. The HTTP Archive's 2024 Web Almanac reports the median page ships around 70 KB of CSS. Bootstrap's 22.1 KB compressed is below average. The real cost is not the number. It is the unshipped features you imported by accident. Use the build step. Measure the output. The last few kilobytes are not worth the engineering time.

The Bottom Line

Bootstrap 5 costs you a build step if you want to customise, a specificity battle if you do not use @layer, and a JavaScript dependency for interactive components. In return, you get a tested, accessible grid, forms, and utilities that work across Baseline browsers. The shipped bytes are the least interesting part. 22.1 KB compressed is not the problem.

The problem is the implicit design decisions baked into the framework. You must override them or accept them. If you can accept them, Bootstrap 5 is a fast way to ship a decent-looking page. If you cannot, walk away now. The utility API's silent failure mode, the @layer pattern that actually works, and the exact byte count let you decide without guessing.