Bulma's Sass Variable Architecture: Build-Time Theming Without Runtime Cost
Bulma's Sass variable system uses !default flags and derived tokens to produce a single themed CSS artifact at build time. Override order matters, breakpoint names are public API, and contrast inversions need manual verification.
17 Oct 2026, 05:38 UTC

Requirements: Design Tokens That Compile Away
Teams adopting Bulma typically need three things: a single CSS artifact for production, brand colors that propagate to every component without manual coordination, and responsive modifiers that extend cleanly when breakpoints change. The constraint is zero JavaScript at runtime — CSP policies, SSR hydration, and edge caching all break if theming requires client-side logic.
Bulma meets this by exposing ~400 Sass variables (colors, spacing, breakpoints, component defaults) all marked !default. A project overrides any subset before @import 'bulma', then recompiles. The result is static CSS with no runtime dependency on Sass, PostCSS, or CSS-in-JS.
Smallest Suitable Design: Override Order and Derived Variables
The core mechanism is two-fold: !default flags on every token, and derived variables that compute inversions, contrasts, and component states from a single source.
// project/_variables.scss
$primary: #1a73e8;
$primary-invert: findColorInvert($primary); // derived automatically
$breakpoints: ("mobile": 0, "tablet": 769px, "desktop": 1024px, "xxl": 1440px);
@import "bulma";
Changing $primary cascades to button text, link hover, tag backgrounds, and .has-background-primary utilities because $primary-invert, $primary-light, $primary-dark, and component-scoped variables like $button-primary-color all derive from it. The $breakpoints map key names become responsive modifier suffixes: adding "xxl" generates .is-hidden-xxl, .column.is-xxl-4, .is-flex-xxl automatically.
Trust and Data Boundaries: Build-Time vs Runtime
The trust boundary sits at the Sass compilation step. All overrides must precede @import 'bulma'; overriding after the import has no effect because !default only applies when a variable is undefined. This surprises developers accustomed to CSS cascade ordering where later declarations win.
Data flows one direction: Sass variables → compiled CSS → browser. Bulma 1.0+ emits :root custom properties mirroring Sass tokens (e.g., --bulma-primary, --bulma-breakpoint-desktop), enabling a hybrid model: brand tokens at build time, user preferences (dark mode, reduced motion) at runtime via [data-theme='dark'] on :root without rebuild.
Operational Checks: Verifying the Compilation Pipeline
Create a minimal Vite + Sass project to validate the override chain:
# Terminal: project root (requires Node 18+, write access to node_modules)
npm create vite@latest bulma-theme-test -- --template vanilla
cd bulma-theme-test
npm i -D sass bulma
# Replace src/style.scss with override block above
npm run build
# Inspect dist/assets/index-*.css for .is-hidden-xxl and derived colors
Run a type-mismatch test to confirm compile-time failure:
# Terminal: any directory with Dart Sass installed (brew install sass/sass/sass)
echo "$primary: 123; @import 'bulma';" > test.scss
sass --load-path=node_modules --style=expanded test.scss test.css
# Expected: Error: $primary: 123 is not a color.
Audit specificity in browser DevTools: .button.is-primary (component class) vs .has-background-primary (utility). The utility uses !important by design; component classes do not. This keeps specificity wars contained to utility overrides.
Failure Modes: Where the Model Breaks
- Override after import:
@import 'bulma'; $primary: red;compiles but ignores the override. No warning — silent failure. - Breakpoint key rename: Changing
"desktop"to"lg"in$breakpointsbreaks every.is-desktop,.column.is-desktop-6in existing markup. Treat breakpoint names as public API. - Dart Sass migration: Switching from
@importto@use 'bulma' with ($primary: red);changes namespace semantics. Legacy builds may silently drop overrides if@forwardchains are incomplete. - Contrast inversion edge cases: Derived
$primary-invertassumes WCAG AA. A brand color like#ffff00(yellow) produces black text on yellow — passes AA for large text but fails for body copy. Verify with axe or Lighthouse. - Token proliferation: Deeply nested variables (
$dropdown-item-hover-background-color) duplicate global tokens. Auditnode_modules/bulma/sass/utilities/_variables.scssbefore adding custom ones.
Conditions That Would Change the Design
- Design-token JSON export needed: Bulma does not ship a
tokens.json. Teams syncing Figma/Storybook must maintain a separate token file or parse_variables.scsswith a custom script. - Runtime theme switching without CSS custom properties: If legacy browser support (pre-
:rootvariable support) is required, the hybrid model fails. Consider a Sass mixin generating multiple theme stylesheets (theme-light.css,theme-dark.css) swapped via<link rel="alternate stylesheet">. - Component library isolation: If Bulma components must coexist with another framework's CSS without global namespace collision, the current flat class naming (
.button,.is-primary) becomes a liability. CSS Modules or Shadow DOM would be required — moving theming out of Sass entirely. - Design-system governance: When token changes require design-review approval, the
!defaultoverride model lacks audit trails. A token registry with PR-gated changes (e.g., Style Dictionary + GitHub Actions) replaces the ad-hoc Sass override file.
Practical Verification Checklist
- Build passes with
sass --style=compressedand no warnings. - Generated CSS contains custom breakpoint classes (
.is-xxl-*). - Derived colors appear in
.button.is-primary,.tag.is-primary,.has-text-primarywithout manual overrides. - Dark-mode toggle via
document.documentElement.dataset.theme = 'dark'propagates--bulma-primaryoverrides without rebuild. - Lighthouse accessibility score ≥ 90 for contrast on all generated component states.
Limitations
This architecture assumes a Sass compilation step in CI/CD. Projects using zero-build tooling (ESM-only, import maps) cannot use Bulma's theming without adding a build pipeline. The :root custom property emission in Bulma 1.0 mitigates this for runtime overrides but does not replace build-time brand token compilation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.