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:
- CSS custom properties as the configuration layer. Bulma 1.0 registers hundreds of
--bulma-*variables at:rootand on component selectors, then builds every rule from them. This is what enables runtime theming, dark mode, and per-subtree overrides without recompiling Sass. - 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.
- Modern Dart Sass modules. The codebase uses
@use/@forwardexclusively (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.
utilitiesandthemesmust come first because every later module reads the--bulma-*variables they define.helperscome last so their!importantutility 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:
- Declares Sass
$variableswith sensible defaults (overridable via@use … with). - Calls
register-vars(...)on its own selector to publish those as--bulma-<component>-*custom properties. - 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-lthrough--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-runssetup-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 toborder-box, strips list styles, collapses table borders.generic.scss— opinionated defaults forhtml,body, links,code,pre,hr,strong, tables, and focus-visible outlines, all sourced from--bulma-*variables.animations.scss— thespinAroundandpulsatekeyframes 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-sassor Ruby Sass engines will fail — the codebase relies on the Dart Sass module system (@use,@forward,with) and the modernsass:color/sass:mathmodules.
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-prefixand 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-activeetc. 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
@forwardonly the modules you actually use (e.g. dropgridorhelpers). - [ ] Use the
bulma-no-helpersprebuilt 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"(ortheme-darkclass) on<html>or any subtree. - Persist a user choice in
localStorageand 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
!defaultvariables, published once viaregister-vars, and consumed viagetVar. Changing a default ripples everywhere consistently. - Strict module boundaries. Files declare their dependencies explicitly with
@usealiases (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 physicalleft/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 helperscontainer-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-sassremain 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/tilescomponent. - No
is-errorcolor 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 ownpackage.json, plus a React-based "Bulma Customizer" underdocs/_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/andbulma.scss. The contents ofcss/are build artifacts regenerated bynpm 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 |