SCSS vs CSS Variables for Theming Bootstrap 5

  • Canvas Team
  • 7 min read
7 min read
Share:

Canvas HTML Template combines them to deliver a robust theming layer out of the box.

Key Takeaways

  • SCSS variables are resolved at compile time; CSS custom properties are resolved at runtime in the browser.
  • Bootstrap 5 ships with both — SCSS maps for the build pipeline and CSS variables on components for live overrides.
  • CSS custom properties enable per-component theming, dark mode, and user-driven color switching without a rebuild.
  • SCSS still wins for logic-heavy theming: loops, functions, maps, and conditional output.
  • A hybrid approach — SCSS to generate the initial values, CSS variables to expose them — gives you the best of both worlds.

How Each Approach Works

Understanding the fundamental difference is essential before you write a single line of theme code. SCSS variables (prefixed with $) exist only during the Sass compilation step. When the build finishes, every reference to $primary has been replaced by a static hex value. The browser never sees the variable name — only the computed output.

CSS custom properties (also called CSS variables, prefixed with --) are part of the CSS specification. They ship in your stylesheet, live in the browser’s style engine, and can be read or overwritten at any time by JavaScript, a media query, or a selector with higher specificity.

// SCSS variable — compiled away at build time
$primary: #0d6efd;

.btn-primary {
  background-color: $primary; // outputs: background-color: #0d6efd;
}
/ CSS custom property — lives in the browser /
:root {
  --bs-primary: #0d6efd;
}

.btn-primary {
  background-color: var(--bs-primary);
}

Bootstrap 5 uses both. The source Sass files are built around $-prefixed variables and maps. Starting in Bootstrap 5.2, the compiled CSS also exposes component-level CSS custom properties so you can override them without touching Sass at all.

Theming with SCSS Variables

The classic Bootstrap theming workflow is straightforward: create a custom Sass partial that overrides variables before importing Bootstrap’s own files, then compile.

// _custom-variables.scss
$primary:   #7c3aed;
$secondary: #0ea5e9;
$font-size-base: 1rem;
$border-radius: 0.5rem;

// Import Bootstrap after your overrides
@import "bootstrap/scss/bootstrap";

Because Bootstrap’s internal Sass uses the !default flag on every variable, your values take precedence. The compiler then propagates them through every mixin, function, and utility class — generating tints, shades, focus rings, and button states automatically from a single source of truth.

SCSS variables are also the correct choice when you need computed values. Bootstrap’s color functions, color-contrast(), and the $theme-colors map all depend on having real values available at compile time so Sass can do the math.

// Extending the theme color map
$theme-colors: map-merge(
  $theme-colors,
  (
    "brand":  #7c3aed,
    "accent": #f59e0b
  )
);

The downside: every time you change a Sass variable, you need a full rebuild. That rules out runtime theming scenarios — user-selectable palettes, live preview editors, or dark mode toggled by a class swap in JavaScript.

Theming with CSS Custom Properties

Bootstrap 5.2 introduced component CSS variables alongside the existing Sass layer. Each major component now declares its own custom properties, scoped to the component selector:

.btn {
  --bs-btn-padding-x: 0.75rem;
  --bs-btn-padding-y: 0.375rem;
  --bs-btn-font-size: 1rem;
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-bg: transparent;
  --bs-btn-border-color: transparent;
}

To restyle a button variant without writing a new Sass partial or recompiling, you simply override the relevant custom properties in plain CSS:

.btn-primary {
  --bs-btn-bg: #7c3aed;
  --bs-btn-border-color: #7c3aed;
  --bs-btn-hover-bg: #6d28d9;
  --bs-btn-active-bg: #5b21b6;
}

Dark mode is where CSS custom properties truly shine. A single class or media query can swap every token across an entire page without any Sass involvement:

@media (prefers-color-scheme: dark) {
  :root {
    --bs-body-bg: #0f172a;
    --bs-body-color: #e2e8f0;
    --bs-border-color: #334155;
  }
}

/ Or with a class toggle via JavaScript /
[data-bs-theme="dark"] {
  --bs-body-bg: #0f172a;
  --bs-body-color: #e2e8f0;
}

The limitation here is that CSS custom properties cannot be used inside Sass functions. You cannot write darken(var(--bs-primary), 10%) — Sass has no idea what that variable will evaluate to. Logic stays in Sass; runtime flexibility lives in CSS variables.

The Hybrid Pattern: SCSS Generates CSS Variables

The most scalable approach uses SCSS to generate and seed CSS custom properties, then lets the browser handle runtime overrides. This is exactly the pattern Bootstrap 5.2 adopted internally.

// Use Sass to build the :root token sheet
:root {
  --color-primary:   #{$primary};
  --color-secondary: #{$secondary};
  --color-surface:   #{$white};
  --color-text:      #{$gray-900};
  --radius-base:     #{$border-radius};
}

// Components consume the CSS variables
.card {
  background-color: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-base);
}

Note the #{} interpolation — this is required to embed a Sass variable’s computed value as a CSS variable string. The result is a :root block with hard-coded hex values that can still be overridden at runtime. You get Sass-powered logic during the build and CSS variable flexibility in the browser.

Canvas Template follows this hybrid strategy. The template exposes --cnvs-themecolor as a top-level CSS custom property seeded from the Sass build, allowing integrators to switch the accent color with a single JavaScript assignment — no rebuild required:

// Runtime theme switch — no Sass rebuild needed
document.documentElement.style.setProperty('--cnvs-themecolor', '#7c3aed');

Performance and Browser Support

Browser support for CSS custom properties is effectively universal — all evergreen browsers have supported them since 2016. Internet Explorer 11 does not, but Bootstrap 5 itself dropped IE support, so this is no longer a concern for the vast majority of projects.

From a performance perspective, SCSS compiles to flat, selector-resolved CSS which can be marginally faster to parse because the browser does not need to resolve variable inheritance. In practice, the difference is negligible for typical web projects. The real performance win from CSS variables is reducing JavaScript-triggered style recalculations — changing one custom property on :root is far cheaper than toggling hundreds of inline styles or swapping entire stylesheets.

When to Use Which Approach

Use SCSS variables when:

  • You need Sass functions, loops, or maps to generate utilities and variants.
  • The theme is set once at build time and never changes for end users.
  • You are extending Bootstrap’s $theme-colors map to auto-generate utility classes.

Use CSS custom properties when:

  • You need runtime theme switching (dark mode, user-selected palettes).
  • You are overriding Bootstrap component styles in a no-build or CDN workflow.
  • You are building a design system where consumers should not need a Sass pipeline.
  • You want per-instance component theming without adding specificity wars.

Use both together when:

  • You have a Sass build pipeline but need to expose tokens for runtime customization.
  • You are building a product where white-labeling or tenant-specific themes must work without deploys.
  • You want Bootstrap’s full utility generation plus a live-preview theme editor.

Frequently Asked Questions

Yes. If you are loading Bootstrap from a CDN, you can override component CSS variables directly in a plain .css file or a <style> block. Scope overrides to :root for global changes or to the component selector for targeted ones. You lose access to Sass maps and utility generation, but for straightforward visual theming this workflow is entirely viable.

Sass functions execute at compile time, before the browser runs. At that point, a CSS custom property is just an unresolved string — Sass has no way to compute its color value. Use Sass variables when you need Sass color math, then expose the computed results as CSS variables using #{} interpolation.

Bootstrap 5.2 added local CSS custom properties to each component. These are declared inside the component’s own selector (e.g., .btn, .card, .navbar) and used throughout that component’s rules. This means you can override a single component’s tokens without affecting anything else, simply by re-declaring the variable on the same selector or a more specific one.

Bootstrap 5.3 introduced a native data-bs-theme attribute that re-assigns all component CSS variables for dark mode. You can apply it to <html> for a global dark theme or to any container element for a scoped dark section. For Bootstrap versions below 5.3, the hybrid approach — overriding :root CSS variables inside a [data-theme="dark"] selector — is the most maintainable strategy.

Yes. Canvas exposes its primary accent as the --cnvs-themecolor CSS custom property. Setting this property on document.documentElement via JavaScript updates the accent color across all Canvas components instantly, without any Sass recompilation or page reload. This makes it straightforward to build live theme pickers or tenant-specific color schemes on top of the template.

Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.

If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.

Skip the setup — build it free

Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.

Share:
Canvas Team
Canvas Team

Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.

More from the Canvas Blog