Jesse A. Green / notebook

Bulma Admin & Architecture Guide

Generated June 2026 from jgthms/bulma at 741da22

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the deep reference for engineers, architects, and design-system maintainers who integrate, customize, build, or extend Bulma v1.0.4 — the modern, CSS-only framework by Jeremy Thomas (jgthms). It documents the Sass module architecture, the CSS custom-property engine that powers theming, the complete customization surface, the build pipeline, security posture, and the internals needed to reproduce or fork the project.


1. Executive Summary

Bulma is a pure CSS framework. Its sole runtime artifact is one stylesheet (css/bulma.css and its minified twin css/bulma.min.css). It ships zero JavaScript and has no runtime dependencies — the published npm package contains only Sass source, compiled CSS, the license, and the README.

Three architectural decisions define the project:

  1. CSS custom properties as the configuration layer. Bulma 1.0 registers hundreds of --bulma-* variables at :root and on component selectors, then builds every rule from them. This is what enables runtime theming, dark mode, and per-subtree overrides without recompiling Sass.
  2. HSL-based, accessibility-aware color generation. Each base color is decomposed into hue/saturation/lightness variables; Sass functions derive 20 numbered shades plus light/dark/soft/bold/invert variants, automatically selecting WCAG-contrasting text/background pairings.
  3. Modern Dart Sass modules. The codebase uses @use/@forward exclusively (no @import), organized into clear layers: utilities → themes → base → elements → form → components → grid → layout → helpers.
Attribute Value
Version 1.0.4
License MIT (© Jeremy Thomas)
Language Sass (Dart Sass), compiled to CSS
Runtime dependencies None
JavaScript None
Entry (Sass) bulma.scss → @use "sass"
Compiled output css/bulma.css (~785 KB), css/bulma.min.css (~678 KB)
Layout engines Flexbox (columns), CSS Grid (grid/cell)
Theming Light + dark via prefers-color-scheme, [data-theme], .theme-*
Browser targets Recent Chrome, Edge, Firefox, Opera, Safari; IE 10+ partial

2. Architecture

2.1 Module graph

The compiled stylesheet is produced from a single root entry point that pulls in one aggregator:

// bulma.scss
@charset "utf-8";
/*! bulma.io v1.0.4 | MIT License | github.com/jgthms/bulma */
@use "sass";

sass/_index.scss forwards the layers in a deliberate cascade order:

@forward "utilities";   // variables, functions, controls (no output on their own)
@forward "themes";      // registers --bulma-* variables (light default + dark + setup)
@forward "base";        // minireset, generic element styles, keyframes
@forward "elements";    // button, tag, title, box, table, …
@forward "form";        // input, select, file, field, control, …
@forward "components";  // navbar, card, modal, dropdown, menu, …
@forward "grid";        // columns + smart grid
@forward "layout";      // container, hero, level, media, section, footer
@forward "base/skeleton";
@forward "helpers";      // utility classes (loaded last so !important wins)

Note

Order matters. utilities and themes must come first because every later module reads the --bulma-* variables they define. helpers come last so their !important utility rules reliably override component defaults.

bulma.scss
└── sass/_index.scss
    ├── utilities/  initial-variables, functions, derived-variables, controls,
    │               css-variables, mixins, extends   (the engine — mostly no CSS output)
    ├── themes/      light.scss, dark.scss, setup.scss  (register --bulma-* values)
    ├── base/        minireset.scss, generic.scss, animations.scss, skeleton.scss
    ├── elements/    block, box, button, content, delete, icon, image, loader,
    │               notification, progress, table, tag, title
    ├── form/        shared, input-textarea, checkbox-radio, select, file, tools
    ├── components/  breadcrumb, card, dropdown, menu, message, modal, navbar,
    │               pagination, panel, tabs
    ├── grid/        columns.scss, grid.scss
    ├── layout/      container, footer, hero, level, media, section
    └── helpers/     aspect-ratio, border, color, flexbox, float, gap, overflow,
                    position, spacing, typography, visibility, other

2.2 The CSS custom-property engine (utilities/css-variables.scss)

This file is the heart of Bulma 1.0. It defines the helper API that every other file uses to register and read variables:

Sass API Purpose
buildVarName($name, $prefix, $suffix) Builds --bulma-<prefix><name><suffix>
getVar($name, $prefix, $suffix) Returns var(--bulma-…) for use in a property
getVarWithBackup($name, $backup, …) var(--a, var(--b)) fallback chain
register-var($name, $value, …) Emits one custom property declaration
register-vars($map, …) Emits a whole map of declarations
register-hsl($name, $color) Splits a color into -h, -s, -l variables
register-rgb($name, $color) Emits an -rgb channel triple
generate-color-palette(...) Produces 20 shades + invert/light/dark/soft/bold for a color
generate-on-scheme-colors(...) Computes an accessible "on scheme" lightness for a color
bulma-theme($name) / system-theme($name) Wraps content in [data-theme]/.theme-* or a prefers-color-scheme query

Components never hard-code colors. Instead each component file:

  1. Declares Sass $variables with sensible defaults (overridable via @use … with).
  2. Calls register-vars(...) on its own selector to publish those as --bulma-<component>-* custom properties.
  3. Builds its rules with getVar(...), so the same compiled CSS can be re-themed at runtime.

A representative excerpt (the box element):

$box-background-color: cv.getVar("scheme-main") !default;
$box-radius: cv.getVar("radius-large") !default;

.box {
  @include cv.register-vars((
    "box-background-color": #{$box-background-color},
    "box-radius": #{$box-radius},
  ));
  background-color: cv.getVar("box-background-color");
  border-radius: cv.getVar("box-radius");
}

2.3 Color system

Colors live as HSL component variables, not opaque hex values. For each color name, Bulma registers --bulma-<name>-h, -s, -l and then generate-color-palette() derives:

  • 20 numbered lightness shades: --bulma-<name>-00-l through --bulma-<name>-100-l (steps of 5), each with a matching --bulma-<name>-<digit> color and an -invert-l.
  • Semantic variants: -invert, -light, -dark, -soft, -bold, plus their inverts.
  • -on-scheme-l: a lightness adjusted (via a luminance contrast loop, target ratio > 5) to remain readable on the current scheme background.

The contrast machinery uses bulmaColorLuminance() (WCAG relative luminance) and bulmaColorBrightness() in utilities/functions.scss. This is why a single $primary: #6c5ce7 override cascades into dozens of accessible, theme-aware tints automatically.

2.4 Theming (sass/themes/)

File Role
light.scss light-theme mixin: registers base hues, lightness scales, type/spacing/radius vars, and generates every color palette for a 100% scheme-main lightness
dark.scss dark-theme mixin: overrides only the lightness anchors (scheme-main-l: 9%, background-l: 14%, text-l: 71%, soft/bold swaps, larger hover deltas) and regenerates on-scheme colors
setup.scss setup-theme mixin: computes derived semantic variables (scheme-main, background, border, text, link, shadow, focus-*, code, pre) from the anchors

The themes index wires these to four targets:

:root { @include light.light-theme; @include setup.setup-theme; }            // default
@include cv.system-theme("light") { @include light.light-theme; }            // prefers-color-scheme: light
@include cv.system-theme("dark")  { @include dark.dark-theme; }              // prefers-color-scheme: dark
@include cv.bulma-theme("light")  { @include light.light-theme; @include setup.setup-theme; } // [data-theme=light], .theme-light
@include cv.bulma-theme("dark")   { @include dark.dark-theme;  @include setup.setup-theme; }  // [data-theme=dark],  .theme-dark

Important

A forced theme (data-theme / .theme-*) always re-runs setup-theme, so derived colors are recomputed for that subtree. Switching themes at runtime is therefore a single attribute change — no stylesheet swap required.

2.5 Base layer

  • minireset.scss — a minimal reset (v0.0.6): zeroes margins/padding on block elements, normalizes box-sizing to border-box, strips list styles, collapses table borders.
  • generic.scss — opinionated defaults for html, body, links, code, pre, hr, strong, tables, and focus-visible outlines, all sourced from --bulma-* variables.
  • animations.scss — the spinAround and pulsate keyframes used by loaders and skeletons.

3. Requirements

Concern Requirement
To use compiled CSS Any modern browser; just link css/bulma.min.css. No tooling.
To customize via CSS variables A browser supporting CSS custom properties (all current browsers). No build step.
To build from Sass Dart Sass (sass npm package, tested against ^1.97). Ruby Sass and Node Sass are not supported.
To minify PostCSS + cssnano (the project uses postcss-cli ^11, cssnano ^7).
Node.js Any current LTS for the npm scripts.
Browser support Recent Chrome, Edge, Firefox, Opera, Safari; IE 10+ partial (Flexbox/Grid required).

Warning

Compiling Bulma 1.0 with the deprecated node-sass or Ruby Sass engines will fail — the codebase relies on the Dart Sass module system (@use, @forward, with) and the modern sass:color / sass:math modules.


4. Customization Reference

There are two override surfaces: Sass variables (build time) and CSS custom properties (runtime).

4.1 Build-time: Sass @use … with

@use "bulma/sass" with (
  $primary: #6c5ce7,
  $link: #0a84ff,
  $family-primary: '"Nunito", sans-serif',
  $radius: 0.5rem,
  $control-border-width: 2px,
  $custom-colors: ("twitter": #1da1f2)
);

Initial variables (utilities/initial-variables.scss)

Variable Default Notes
$scheme-h / $scheme-s 221 / 14% Base hue/saturation for greys & scheme
$dark-l / $light-l 20% / 90% Lightness anchors for soft/bold
$black…$white HSL greys at 4%→100% Nine-step grey scale (black, black-bis, black-ter, grey-darker…grey-lightest, white-ter, white-bis, white)
$orange hsl(14,100%,53%) Palette color
$yellow hsl(42,100%,53%)
$green hsl(153,53%,53%)
$turquoise hsl(171,100%,41%) Default primary
$cyan hsl(198,100%,70%) Default info
$blue hsl(233,100%,63%) Default link
$purple hsl(271,100%,71%)
$red hsl(348,100%,70%) Default danger
$family-sans-serif Inter-led stack "Inter","SF Pro","Segoe UI","Roboto",…
$family-monospace Inconsolata-led stack "Inconsolata","Hack","SF Mono",…
$size-1…$size-7 3rem,2.5rem,2rem,1.5rem,1.25rem,1rem,0.75rem Type scale
$weight-light…$weight-extrabold 300,400,500,600,700,800 Font weights
$block-spacing 1.5rem Default gap below blocks
$aspect-ratios 15 pairs (1,1),(4,3),(16,9),…
$gap 32px Container gutter & breakpoint offset
$tablet 769px Breakpoint
$desktop 960px + 2*$gap = 1024px Breakpoint
$widescreen 1152px + 2*$gap = 1216px Breakpoint ($widescreen-enabled)
$fullhd 1344px + 2*$gap = 1408px Breakpoint ($fullhd-enabled)
$duration 294ms Transition duration
$easing ease-out
$radius-small…$radius-rounded 0.25rem,0.375rem,0.5em,0.75rem,9999px Corner radii
$speed 86ms
$variable-columns true Enable variable column gaps
$rtl false Right-to-left flag (partial support)
$class-prefix "" Prefix prepended to every class
$cssvars-prefix "bulma-" Prefix on every custom property
$helpers-prefix / $helpers-has-prefix "is-" / "has-" Helper class prefixes
$variables-host ":root" Selector that hosts global variables

Derived variables (utilities/derived-variables.scss)

Map raw palette colors to semantic roles, all overridable:

Variable Default source
$primary $turquoise
$link $blue
$info $cyan
$success $green
$warning $yellow
$danger $red
$light / $dark $white-ter / $grey-darker
$text / $text-strong / $text-weak $grey-dark / $grey-darker / $grey
$background / $border / $border-weak $white-ter / $grey-lighter / $grey-lightest
$family-primary / $family-secondary / $family-code sans / sans / monospace
$size-small…$size-large $size-7,$size-6,$size-5,$size-4
$colors (map) white,black,light,dark,text,primary,link,info,success,warning,danger
$shades (map) the nine grey steps
$custom-colors / $custom-shades null (your extension points)

Tip

To add a brand color that flows into every is-/has- class, pass $custom-colors: ("brand": #ff3860). mergeColorMaps() accepts either a single color (Bulma derives invert/light/dark) or a list (base, invert, light, dark) for full control.

4.2 Runtime: CSS custom properties

Every value above is also published as --bulma-* and can be overridden in plain CSS at any scope:

:root {
  --bulma-primary-h: 270deg;
  --bulma-primary-s: 100%;
  --bulma-primary-l: 60%;
  --bulma-radius: 1rem;
  --bulma-link-h: 210deg;
}

/* Scoped override — only this subtree */
.promo {
  --bulma-body-background-color: #111;
}

Per-component variables follow the pattern --bulma-<component>-<property> (e.g. --bulma-button-padding-horizontal, --bulma-card-radius, --bulma-navbar-height). Component files register dozens each; inspect the relevant sass/**/<name>.scss register-vars block for the exhaustive list.


5. Build & Deployment Pipeline

All build logic lives in package.json scripts (the framework package.json, not the docs one):

Script Command Purpose
build-bulma sass --style=expanded --source-map bulma.scss css/bulma.css Compile the main stylesheet
minify-bulma postcss css/bulma.css --no-map --use cssnano --output css/bulma.min.css Minify
version-no-dark-mode sass … versions/bulma-no-dark-mode.scss … Compile the light-only variant
version-no-helpers sass … versions/bulma-no-helpers.scss … Compile without helpers
version-no-helpers-prefixed sass … versions/bulma-no-helpers-prefixed.scss … No helpers + class prefix
version-prefixed sass … versions/bulma-prefixed.scss … Class-prefixed variant
build-versions runs all four version-* Build every variant
minify-versions postcss css/versions/*.css … --use cssnano Minify all variants
build-all / minify-all chains build + versions / minify + versions Full build
clean rimraf css Remove generated CSS
deploy clean && build-all && minify-all Full reproducible build
start build-bulma -- --watch Watch mode for development
test sass --style=expanded --source-map --watch test.scss test.css Compile the Sass test fixture

5.1 Prebuilt variants (versions/)

File Definition When to ship it
bulma-prefixed.scss @use "../sass" with ($class-prefix: "bulma-") Embedding Bulma alongside another framework — all classes become .bulma-button, etc.
bulma-no-dark-mode.scss Forwards all modules but only the light theme Apps that never use dark mode (smaller output)
bulma-no-helpers.scss Forwards all modules except helpers Trim the large helper surface
bulma-no-helpers-prefixed.scss No helpers + prefix Combination of the above

Note

The CSS-variable prefix (--bulma-) is fixed by $cssvars-prefix and is independent of $class-prefix. Prefixing classes does not rename the custom properties.

5.2 Reproducing the published artifacts

git clone https://github.com/jgthms/bulma
cd bulma
npm install          # installs Dart Sass, postcss-cli, cssnano, prettier, rimraf
npm run deploy       # clean → build-all → minify-all  ⇒ css/ + css/versions/

The files array in package.json defines what npm publishes: css, sass, versions, bulma.scss, LICENSE, README.md. Nothing else (no docs site, no tests) ships to consumers.


6. Security Analysis

Bulma's attack surface is exceptionally small because it is a static stylesheet.

Vector Assessment
JavaScript execution None. No JS is shipped, so there is no script-injection or prototype-pollution surface from Bulma itself.
Runtime dependencies None. The published package has zero dependencies; supply-chain exposure is limited to build-time devDependencies (sass, postcss-cli, cssnano, prettier, rimraf).
CSS injection If you interpolate untrusted values into Bulma class names or into --bulma-* variable overrides, standard CSS-injection caution applies — never build class strings or inline style custom properties from unsanitized user input.
Content sourcing Loading from a CDN introduces a third-party trust boundary; pin a version and add Subresource Integrity (SRI) if your threat model requires it.
Clickjacking / overlay Components like modal-background and is-overlay create full-viewport layers; ensure your own JS enforces focus trapping and dismissal — Bulma only provides the styling.

Caution

Interactive components are styled, not behaviorally secured. Accessibility and security of modals, dropdowns, and menus (focus management, aria-*, escape handling, dismissal) are the integrator's responsibility because Bulma ships no JavaScript.


7. Operations

7.1 Integration patterns

  • Static / CDN: link bulma.min.css. Zero build. Best for content sites.
  • Bundler (Vite/webpack/Rollup): import "bulma/css/bulma.min.css" or import the Sass entry for customization.
  • Component frameworks (React/Vue/Svelte): Bulma is class-only, so it composes cleanly. Toggle is-active etc. via your framework's state.
  • Design system: compile from sass/ with @use … with (...), forwarding only the modules you use to control bundle size.

7.2 Reducing stylesheet size

Because Bulma 1.0 emits a comprehensive variable and helper surface, the compiled CSS is large (~678 KB minified). Strategies:

  • [ ] Build from Sass and @forward only the modules you actually use (e.g. drop grid or helpers).
  • [ ] Use the bulma-no-helpers prebuilt variant.
  • [ ] Enable gzip/brotli on your server — the variable-heavy CSS compresses very well.
  • [ ] Run a CSS tree-shaker (e.g. PurgeCSS) against your final markup as a build step.

7.3 Theming operations

  • Default theme follows prefers-color-scheme.
  • Force a theme by setting data-theme="dark" (or theme-dark class) on <html> or any subtree.
  • Persist a user choice in localStorage and apply it on load: document.documentElement.dataset.theme = saved.

8. Code Quality & Conventions

  • Single source of truth per value. Defaults are declared once as Sass !default variables, published once via register-vars, and consumed via getVar. Changing a default ripples everywhere consistently.
  • Strict module boundaries. Files declare their dependencies explicitly with @use aliases (cv = css-variables, iv = initial-variables, dv = derived-variables, mx = mixins). No global leakage.
  • Logical properties throughout. margin-inline, inset-inline, border-start-*-radius, etc., give partial RTL support and better internationalization than physical left/right.
  • Placeholders + mixins for shared behavior (%control, %block, %unselectable, %overlay, %loader, %arrow, %delete) keep the output DRY via @extend.
  • Responsive mixins (mobile, tablet, desktop, widescreen, fullhd, touch, *-only, plus container-query helpers container-from/container-until) centralize breakpoint logic.
  • Formatting is enforced with Prettier (the docs site also uses a Liquid plugin).
  • Legacy test fixtures in test/dart-sass, test/node-sass, test/ruby-sass remain in the tree for historical compilation checks; only Dart Sass is supported in 1.0.

9. Potential Improvements & Known Issues

Drawn from the source and the project's open community issues.

Item Detail Source
is-offset-one-third offset bug Emits margin-inline-start: 0.3333% instead of 33.3333% for is-offset-one-third and its -mobile/-tablet/-touch/-desktop/-widescreen variants. Only -fullhd is correct. Workaround: use is-offset-4. Issue #4044; sass/grid/columns.scss
--bulma-block-spacing can be undefined Some components read it where it is not globally registered, collapsing block spacing. Mitigate by defining it on :root. Issue #4037
Bundle size Comprehensive CSS-variable + helper surface makes even minified output large (~678 KB). A documented "lite" build or first-class tree-shaking guidance would help. Issue #3761
Menu component RTL The menu component does not fully support right-to-left layouts. Issue #4029
No error color No dedicated is-error semantic color (community request; is-danger is the current equivalent). Issue #4027
antialiased smoothing generic.scss sets -webkit-font-smoothing: antialiased, which some consider degrades rendering. Issue #4040
Removed tile component The tile layout was removed in 1.0; older tutorials referencing it no longer apply. Use the Smart Grid or columns. sass/ (no tile.scss); issue #4016
Release filename naming Community request to change the release archive filename convention. Issue #4038

Negative knowledge (what does NOT exist in v1.0.4)

  • No JavaScript of any kind.
  • No .tile / tiles component.
  • No is-error color modifier.
  • No support for Node Sass or Ruby Sass compilation.
  • No runtime npm dependencies.

10. Contributing / Forking

The project is a single-package repository:

  • Framework source: sass/ (the product), bulma.scss (entry), versions/ (prebuilt variants), css/ (generated — do not edit by hand).
  • Documentation site: docs/ — a separate Jekyll (Ruby) site with its own package.json, plus a React-based "Bulma Customizer" under docs/_react/bulma-customizer. The docs build is independent of the framework build.

To work on the framework:

npm install
npm start            # watch-compile bulma.scss → css/bulma.css

To work on the docs site (per docs/README.md): install Ruby 2.x and Jekyll, copy _config.yml to _config.local.yml, point its url: at http://localhost:4000, then run jekyll serve --incremental --config _config.local.yml. A docker-compose.yml using the jekyll/jekyll:pages image is provided as an alternative. Note: changing the framework Sass requires npm run start to rebuild the docs' CSS; Jekyll then serves it.

Note

Edits belong in sass/ and bulma.scss. The contents of css/ are build artifacts regenerated by npm run deploy; never patch them directly.


11. Quick Reference

Build commands

npm run build-bulma     # compile main stylesheet
npm run minify-bulma    # minify with cssnano
npm run build-all       # main + all variants
npm run deploy          # clean + build-all + minify-all (full reproducible build)
npm start               # watch mode

Customization cheat sheet

// Build-time
@use "bulma/sass" with (
  $primary: #6c5ce7,
  $radius: 0.5rem,
  $class-prefix: "bulma-",
  $custom-colors: ("brand": #ff3860)
);
/* Runtime */
:root {
  --bulma-primary-h: 270deg;
  --bulma-radius: 1rem;
}
html[data-theme="dark"] { /* dark overrides applied automatically */ }

Breakpoints

Name From Until
mobile — 768px
tablet 769px —
tablet-only 769px 1023px
touch — 1023px
desktop 1024px —
desktop-only 1024px 1215px
widescreen 1216px —
widescreen-only 1216px 1407px
fullhd 1408px —

Module layers (cascade order)

utilities → themes → base → elements → form → components → grid → layout → skeleton → helpers

Key file map

Path Responsibility
bulma.scss Root entry (@use "sass")
sass/_index.scss Layer aggregation order
sass/utilities/initial-variables.scss Build-time Sass variables
sass/utilities/derived-variables.scss Semantic color/type maps
sass/utilities/css-variables.scss The register-var/getVar engine + palette generators
sass/utilities/functions.scss Color/luminance/contrast math
sass/utilities/mixins.scss Responsive + behavior mixins
sass/themes/{light,dark,setup}.scss Theme variable registration
sass/{elements,components,form,grid,layout,helpers}/ The class library
versions/*.scss Prebuilt distribution variants