Building a Lightweight Design System with Bulma's Modular Sass
Bulma's modular Sass architecture lets you import only the components you need and customize design tokens before compilation — producing a tiny, tailored stylesheet without JavaScript dependencies or content-scanning build steps.
06 Mar 2026, 19:07 UTC

The problem with "batteries included" CSS frameworks
Most CSS frameworks ship as a single monolithic file. You import the whole thing, use maybe 20% of it, and ship the rest to every user. Tailwind solves this with PurgeCSS, but that adds a build-step dependency on content scanning. Bulma takes a different approach: it's distributed as a granular Sass library where every component lives in its own file under bulma/sass/. You @use only what you need, and the compiler does the dead-code elimination for you.
How the modular architecture works
Inside node_modules/bulma/sass/ you'll find directories like grid/, elements/, components/, and utilities/. Each component — button.sass, navbar.sass, modal.sass — is a standalone module with its own variables. The entry point bulma.sass simply @forwards everything. For production, you create your own entry file (say my-bulma.sass) that pulls in only the modules your project actually uses.
Because Bulma uses Dart Sass's module system (@use with namespaces) since v0.9, you get explicit dependency tracking. Legacy @import paths still work but emit deprecation warnings.
Design tokens live in variables — override before you import
All design tokens (colors, spacing, breakpoints, typography scales, border radii, shadows) are defined as Sass variables in bulma/sass/utilities/_all.sass and component-specific variable files. The critical rule: overrides must precede the first @use 'bulma/...' statement. Once a module is loaded, its variables are frozen; changing them afterward has no effect.
This means your custom entry file starts with variable overrides, then imports. For example, setting $primary: #2d6a4f before importing the button module recolors every button variant globally without writing a single line of custom CSS.
Worked example: a minimal custom build
Create a Vite project, install bulma and sass, then write src/styles/my-bulma.scss:
// 1. Override design tokens FIRST
$primary: #2d6a4f;
$family-sans: "Inter", system-ui, sans-serif;
$spacing: 1rem;
// 2. Import only what you need
@use 'bulma/sass/grid/columns';
@use 'bulma/sass/elements/button';
@use 'bulma/sass/elements/title';
@use 'bulma/sass/components/navbar';
@use 'bulma/sass/components/dropdown'; // navbar depends on this
@use 'bulma/sass/elements/icon'; // navbar also expects icon variables
Compile with Dart Sass (sass src/styles/my-bulma.scss dist/css/bundle.css). The output contains only the grid, button, title, navbar, and dropdown styles — a fraction of the full framework's size. Inspect the compiled CSS and you should see .button.is-primary using your #2d6a4f value.
Trade-offs you'll hit
- Component dependencies aren't automatic. The example above includes
dropdownandiconbecausenavbarreferences their variables. The docs list required sibling modules for each component; skip one and compilation fails with a missing-variable error. - Dark mode adds weight. Setting
$color-mode: truebefore import emits[data-theme="dark"]scoped overrides for every color-dependent component — roughly 15 KB gzipped extra. Enable it only if you actually need it. - CSS-only interactivity has limits. Modals, dropdowns, and the navbar burger use
:target,:focus-within, and sibling combinators. They lack focus trapping and ARIA management. Production apps typically layer a tiny JS library (e.g.,focus-trap) on top. - Custom breakpoints require map fidelity. The
$breakpointsmap keys (mobile,tablet,desktop,widescreen,fullhd) must stay the same; responsive modifiers for missing keys simply won't generate.
Start small, verify the output
Add a custom entry file to your project, override one color, import two components, and compile. Open the generated CSS and confirm your color appears and unrelated components (like .modal or .tabs) are absent. That five-minute check proves the modular pipeline works before you commit to the pattern across the codebase.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.