Customizing Bootstrap 5 Themes with CSS Custom Properties: A No-Build Approach
Customize Bootstrap 5 themes at runtime using CSS custom properties—no Sass build required. Override design tokens for colors, spacing, and typography with a stylesheet loaded after Bootstrap, enable dark mode and multi-brand theming via scoped variables, and verify changes in DevTools.
13 Feb 2026, 02:02 UTC

The Problem: Theming Without a Build Step
Bootstrap 5 introduced CSS custom properties (variables) as a first-class theming mechanism, yet many teams still reach for Sass compilation to change colors or spacing. If you need to ship a design system that adapts at runtime—dark mode toggles, multi-brand deployments, or per-tenant theming—a build step becomes a liability. CSS variables let you override Bootstrap's design tokens in the browser, no compiler required.
Desired Outcome
A maintainable, runtime-adjustable theme using Bootstrap 5's CSS variables for colors, spacing, and typography. Changes apply instantly via a custom stylesheet or JavaScript, work across modern browsers, and degrade gracefully when variables aren't supported.
Prerequisites
- Bootstrap 5.0+ CSS included (via CDN, npm, or downloaded)
- Basic CSS knowledge—specifically cascade, specificity, and custom property syntax
- No Sass, PostCSS, or build tooling required
- Target browsers supporting CSS custom properties (all modern browsers; IE11 not supported)
Procedure: Override Root Variables After Bootstrap Loads
1. Include Bootstrap CSS First
Load Bootstrap's CSS before your custom stylesheet so its variable definitions exist:
<!-- In your HTML head -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/theme.css" rel="stylesheet">
If using npm, import Bootstrap CSS in your entry point before your theme file.
2. Define Overrides in a Custom Stylesheet
Create theme.css and override Bootstrap's root variables. Bootstrap 5.3+ defines over 200 variables; focus on the core design tokens:
/* theme.css - loaded after Bootstrap */
:root {
/* Brand colors */
--bs-primary: #2563eb;
--bs-primary-rgb: 37, 99, 235;
--bs-secondary: #64748b;
--bs-success: #16a34a;
--bs-danger: #dc2626;
/* Surface colors */
--bs-body-bg: #f8fafc;
--bs-body-color: #1e293b;
--bs-border-color: #e2e8f0;
/* Spacing scale */
--bs-gutter-x: 1.5rem;
--bs-gutter-y: 1.5rem;
/* Typography */
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--bs-body-font-size: 1rem;
--bs-body-line-height: 1.6;
/* Border radius */
--bs-border-radius: 0.5rem;
--bs-border-radius-lg: 0.75rem;
}
The --bs-primary-rgb companion variable enables Bootstrap's rgba(var(--bs-primary-rgb), 0.15) utility patterns. Always pair color variables with their -rgb counterparts when they exist.
3. Scope Themes for Multi-Brand or Dark Mode
Use additional selectors to scope variable overrides. This enables runtime switching via class toggles:
/* Dark mode via media query */
@media (prefers-color-scheme: dark) {
:root {
--bs-body-bg: #0f172a;
--bs-body-color: #f1f5f9;
--bs-border-color: #334155;
--bs-card-bg: #1e293b;
--bs-card-border-color: #334155;
}
}
/* Explicit dark mode class (for manual toggle) */
[data-bs-theme="dark"] {
--bs-body-bg: #0f172a;
--bs-body-color: #f1f5f9;
--bs-border-color: #334155;
--bs-card-bg: #1e293b;
--bs-card-border-color: #334155;
}
/* Per-brand scope */
.brand-acme {
--bs-primary: #7c3aed;
--bs-primary-rgb: 124, 58, 237;
--bs-btn-primary-bg: #7c3aed;
--bs-btn-primary-border: #7c3aed;
}
.brand-globex {
--bs-primary: #ea580c;
--bs-primary-rgb: 234, 88, 12;
--bs-btn-primary-bg: #ea580c;
--bs-btn-primary-border: #ea580c;
}
Toggle themes in JavaScript by swapping the data-bs-theme attribute on <html> or a container, or by adding brand classes.
4. Reference Variables in Custom Components
Your own CSS can consume the same tokens for consistency:
.hero-banner {
background-color: var(--bs-primary);
color: var(--bs-white);
padding: calc(var(--bs-gutter-y) * 3) var(--bs-gutter-x);
border-radius: var(--bs-border-radius-lg);
}
.hero-banner .btn {
--bs-btn-bg: var(--bs-white);
--bs-btn-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-200);
}
Expected Checks
Verify Variable Overrides in DevTools
- Open browser DevTools (F12) → Elements tab
- Select
<html>or:rootin the DOM tree - In Computed styles, filter for "--bs-" to see resolved values
- Confirm your overrides appear and aren't crossed out
Component Reflection Test
Add a test page exercising core components:
<div class="container py-5">
<button class="btn btn-primary">Primary Button</button>
<button class="btn btn-secondary ms-2">Secondary</button>
<div class="card mt-4" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">Card Title</h5>
<p class="card-text">Card content with themed border.</p>
</div>
</div>
<div class="alert alert-success mt-3" role="alert">
Success alert uses --bs-success
</div>
</div>
Verify buttons, cards, and alerts reflect your custom colors, border radius, and spacing.
Responsive Breakpoint Validation
Resize viewport across Bootstrap's breakpoints (576px, 768px, 992px, 1200px, 1400px). Confirm container widths, gutter spacing, and utility classes respond correctly. Your --bs-gutter-x override should affect grid gutters at all breakpoints.
Contrast Ratio Verification
Run automated accessibility checks (axe-core, Lighthouse) or use a contrast checker on:
- Primary button text on primary background
- Body text on body background
- Link colors in light and dark modes
Limitations and Gotchas
Hardcoded Values in Components
Not all Bootstrap components use variables. As of 5.3, some components still have hardcoded values:
- Carousel control icons (SVG fill colors)
- Some form validation icon colors
- Dropdown divider border color
- Progress bar background
Flash of Unstyled Content (FOUC)
If you toggle themes via JavaScript after page load, users may see default colors briefly. Mitigate by:
- Setting initial theme class server-side or via inline script in
<head> - Using
prefers-color-schememedia query for default dark mode - Adding
visibility: hiddento body until theme initializes (with a timeout fallback)
Specificity Conflicts
Bootstrap's utility classes (e.g., .bg-primary) use var(--bs-primary) but have high specificity. If your overrides don't apply:
- Ensure your stylesheet loads after Bootstrap
- Use
:rootorhtmlselector for root overrides - Avoid
!important; increase specificity withhtml:rootorbodyprefix if needed
Maintenance Overhead
Overriding 50+ variables creates maintenance burden. Start with the core 15-20 tokens (primary, secondary, success, danger, warning, info, body-bg, body-color, border-color, font-family, border-radius, gutter, breakpoints) and expand only when components demand it.
Recovery Options
Revert to DefaultsRemove or disable your custom stylesheet. Bootstrap's default variables remain intact in its CSS.
Fallback to Bootstrap Defaults Per-Component
Scope a reset to specific components:
.reset-to-default {
--bs-primary: #0d6efd; /* Bootstrap 5.3 default */
--bs-primary-rgb: 13, 110, 253;
--bs-body-bg: #fff;
--bs-body-color: #212529;
}
Apply .reset-to-default to a container to isolate legacy sections.
Scoped Variable Isolation
If third-party widgets conflict, wrap them in a scope that restores Bootstrap defaults:
.widget-container {
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
/* ... other defaults */
}
Practical Verification Checklist
- [ ] Custom stylesheet loads after Bootstrap (network tab)
- [ ] DevTools shows overridden values on
:root(not crossed out) - [ ] Buttons, alerts, cards, navbars reflect new colors
- [ ] Dark mode toggle switches variables without reload
- [ ] Contrast ratios pass WCAG AA in both themes
- [ ] No FOUC on initial load or theme switch
- [ ] Responsive grid gutters respect
--bs-gutter-x - [ ] Custom components using variables render correctly
When to Consider Sass Instead
CSS variables excel at runtime theming. Consider Sass compilation when:
- You need to strip unused Bootstrap components for bundle size
- Your design system requires compile-time calculations (color mixing, complex math)
- You must support IE11 or legacy browsers without polyfills
- Your team already has a mature Sass workflow and no runtime theming needs
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.