CSS Variables
Bulma v1 introduces comprehensive support for CSS custom properties (CSS variables), enabling runtime customization without Sass compilation. bestax-bulma gives you a React surface for them through the Theme component: named, camelCase props for the scheme and color variables, a typed bulmaVars object for the rest, and automatic scoping so nested themes compose through the CSS cascade — no manual style-sheet injection, no string concatenation, fully type-checked.
The authoritative documentation for every Bulma CSS variable lives on the Bulma site:
- CSS Variables in Bulma — how Bulma uses CSS variables and their
--bulma-prefix. - Themes — Bulma's theme system (a theme is a set of CSS variables scoped at
:rootor below). - Dark Mode — the prefers-color-scheme and
.theme-darkconventions Bulma ships.
This page focuses on the React surface (Theme, bulmaVars); refer to Bulma's docs for the variable catalog and CSS-level semantics.
What are CSS Variables?
CSS variables (officially called CSS custom properties) are entities defined by CSS authors that contain specific values to be reused throughout a document. Unlike Sass variables, which are compile-time constants, CSS variables are runtime values that can be:
- Changed dynamically with JavaScript
- Inherited through the CSS cascade
- Scoped to specific DOM elements
- Modified without rebuilding your CSS
Runtime Customization
Browser DevTools
You can modify Bulma's appearance directly in browser developer tools by changing CSS variable values. This is powerful for testing and debugging:
Steps to test:
- Open your browser's developer tools
- Select the
htmlelement in the Elements panel - In the Styles panel, add or modify a CSS variable like
--bulma-primary-h: 270deg - Watch the changes apply instantly across your entire application
/* Example: Change primary color to purple in DevTools */
:root {
--bulma-primary-h: 270deg;
--bulma-primary-s: 100%;
--bulma-primary-l: 50%;
}
JavaScript Runtime Changes
CSS variables can be modified programmatically:
// Change primary color dynamically
document.documentElement.style.setProperty('--bulma-primary-h', '120deg');
// Change theme scheme
document.documentElement.style.setProperty('--bulma-scheme-h', '210deg');
Using the Theme Component
Theme is bestax's React wrapper for Bulma's CSS variables. Two things make it idiomatic to use from React rather than hand-writing style.setProperty calls:
- Named props for the scheme and color variables —
schemeH,primaryH,linkS,dangerL, and the rest of the HSL set. TypeScript autocompletes them and catches typos at build time. bulmaVarsfor everything else — typography (--bulma-family-primary), radius (--bulma-radius), spacing, and the rest, keyed by their full--bulma-*name. The keys are typed too: written as an object literal, they autocomplete and a misspelled one is a type error.
A Theme changes a variable only where the component inherits it. Bulma declares the scheme, color, typography, radius and spacing tokens on :root, so a Theme reaches the components under it. Some of those Bulma also declares again on one component, which keeps its own value and lists the variable on its API page. Component variables such as --bulma-card-radius or --bulma-delete-dimensions are a different case: Bulma declares them on the component's own element, and a value set on any ancestor, a Theme included, never beats that. bulmaVars still accepts those keys, and warns in development when given one, but they belong on the component itself; see Component-Specific Variables.
Radius and shadow have no CSS-variable props because both names are already helper props: radius is the border radius helper and shadow is the shadowless helper. Set --bulma-radius and --bulma-shadow through bulmaVars.
Themes nest naturally: outer <Theme> sets app-wide defaults, inner ones scope overrides to a subtree. Use isRoot to inject variables at :root for true app-wide reach.
Global Theme Application
import { Theme, Button, Box, Title } from '@allxsmith/bestax-bulma';
function App() {
return (
<Theme
isRoot
primaryH="270"
primaryS="100%"
primaryL="50%"
schemeH="260"
schemeS="30%"
>
<Box p="4">
<Title>Purple-themed Application</Title>
<Button color="primary">Purple Button</Button>
</Box>
</Theme>
);
}
Scoped Theme Application
function ScopedTheming() {
return (
<div>
<Title>Standard Theme</Title>
<Theme primaryH="120" primaryS="100%" primaryL="25%">
<Box p="4" mt="3">
<Title size="4">Green Section</Title>
<Button color="primary">Green Button</Button>
</Box>
</Theme>
<Theme primaryH="15" primaryS="85%" primaryL="40%">
<Box p="4" mt="3">
<Title size="4">Orange Section</Title>
<Button color="primary">Orange Button</Button>
</Box>
</Theme>
</div>
);
}
Using bulmaVars Object
For less common variables or when you have many to set. Bulma derives the body font and the
control radius from these variables at :root, so typography and radius belong on an
isRoot theme:
function AdvancedTheming() {
const customTheme = {
'--bulma-family-primary': '"Helvetica Neue", sans-serif',
'--bulma-family-code': '"Fira Code", monospace',
'--bulma-size-normal': '16px',
'--bulma-weight-bold': '700',
'--bulma-shadow': '0 8px 32px rgba(0, 0, 0, 0.1)',
'--bulma-radius': '12px',
};
return (
<Theme isRoot bulmaVars={customTheme}>
<Box p="4">
<Title>Custom Typography & Styling</Title>
<Button color="primary">Custom Styled Button</Button>
</Box>
</Theme>
);
}
Complete CSS Variables Catalog
Bulma v1 provides its CSS variables organized by category. Here are the key categories — for the full, authoritative list see the Bulma CSS Variables reference and the per-component pages under bulma.io/documentation.
Scheme Variables
| Variable | Description | Example Value |
|---|---|---|
--bulma-scheme-h | Base hue for color scheme | 210 |
--bulma-scheme-s | Base saturation | 50% |
--bulma-light-l | Light background lightness | 96% |
--bulma-dark-l | Dark background lightness | 4% |
--bulma-soft-l | Soft color lightness | 85% |
--bulma-bold-l | Bold color lightness | 15% |
Color Variables
| Variable | Description | Example Value |
|---|---|---|
--bulma-primary-h | Primary color hue | 171 |
--bulma-primary-s | Primary color saturation | 100% |
--bulma-primary-l | Primary color lightness | 41% |
--bulma-link-h | Link color hue | 233 |
--bulma-info-h | Info color hue | 198 |
--bulma-success-h | Success color hue | 153 |
--bulma-warning-h | Warning color hue | 42 |
--bulma-danger-h | Danger color hue | 348 |
Label and Text Lightnesses
The trio recolors the color itself. The label on a solid color (--bulma-<color>-invert-l) and
the color's text on the page (--bulma-<color>-on-scheme-l, which for link is every link on
the page) keep lightnesses Bulma's Sass build picked by contrast with the color it was built
with. bestax's stylesheet builds its primary from a dark blue, so a light primary set through the
trio alone keeps white labels it can't carry. A color that reads lighter or darker than the built
one sets both too, through Theme's bulmaVars or in your CSS. The on-scheme lightness has to
read on a light page and on a dark one, so Bulma gives each scheme its own, and on an isRoot
Theme its key takes one value per scheme:
<Theme
isRoot
primaryH="171deg"
primaryS="100%"
primaryL="41%"
bulmaVars={{
'--bulma-primary-invert-l': 'var(--bulma-primary-05-l)',
'--bulma-primary-on-scheme-l': { light: '21%', dark: '41%' },
}}
>
<App />
</Theme>
The Theme API page has the rest of how
Theme writes them, and the bestax-theming skill's
CSS variables reference
says what reads each one, how to pick the values, and how to set them in CSS.
Typography Variables
| Variable | Description | Example Value |
|---|---|---|
--bulma-family-primary | Primary font family | 'BlinkMacSystemFont', 'Segoe UI', 'Roboto' |
--bulma-family-code | Code font family | 'Source Code Pro', monospace |
--bulma-size-normal | Normal text size | 1rem |
--bulma-weight-normal | Normal font weight | 400 |
--bulma-weight-bold | Bold font weight | 700 |
Layout Variables
| Variable | Description | Example Value |
|---|---|---|
--bulma-block-spacing | Block element spacing | 1.5rem |
--bulma-radius | Default border radius | 4px |
--bulma-radius-rounded | Rounded border radius | 9999px |
--bulma-column-gap | Column spacing | 0.75rem |
Component-Specific Variables
Bulma declares these on the component's own class, so neither a Theme nor :root can change them: a value set on an ancestor loses to the component's own declaration. Set them in your own stylesheet on that class, loaded after the library styles (mind the class prefix under a classPrefix), on a class you add through className, or with the style prop. Each component's API page lists all of its variables under CSS & Sass Variables and says where Bulma declares them.
/* Rounder cards everywhere */
.card {
--bulma-card-radius: 1.5rem;
}
Button Variables
bulmaVars does not accept the button variables, and Bulma declares them on the button's own class anyway, so set them in your own stylesheet on that class (.button, or its prefixed form under a classPrefix). Button corners come from the --bulma-radius scale, which bulmaVars does accept.
| Variable | Description |
|---|---|
--bulma-button-padding-vertical | Button vertical padding |
--bulma-button-padding-horizontal | Button horizontal padding |
--bulma-button-focus-box-shadow-size | Focus shadow size |
Card Variables
| Variable | Description |
|---|---|
--bulma-card-color | Card text color |
--bulma-card-background-color | Card background |
--bulma-card-shadow | Card drop shadow |
--bulma-card-radius | Card border radius |
Input Variables
| Variable | Description |
|---|---|
--bulma-input-color-l | Input text lightness |
--bulma-input-background-l | Input background lightness |
--bulma-input-border-l | Input border lightness |
--bulma-input-focus-shadow-size | Focus shadow size |
Dynamic Theming Examples
Dark Mode Toggle
Bulma ships a built-in dark-mode scheme. The simplest way to drive it is the Theme component's
colorMode prop ('light' | 'dark' | 'system'), which writes Bulma's data-theme attribute on
<html> for you, plus its prefixed form under a class
prefix. 'system' follows the OS prefers-color-scheme.
function DarkModeApp() {
const [mode, setMode] = useState<'light' | 'dark' | 'system'>('system');
return (
<Theme isRoot colorMode={mode}>
<Box p="4">
<Title>Dynamic Dark Mode</Title>
<Button onClick={() => setMode('dark')} color="primary">
Dark
</Button>
<Button onClick={() => setMode('light')} color="primary">
Light
</Button>
<Button onClick={() => setMode('system')}>System</Button>
</Box>
</Theme>
);
}
colorMode writes the attribute before the browser paints what Theme rendered, but the page can
paint before the app's script runs at all, with whatever <html> already has: the OS scheme,
when nothing set one. A toggle that remembers the visitor's choice writes it before that frame, from an inline script in
index.html's <head>, and starts its state from the attribute the script wrote. The
bestax-theming skill's
dark-mode example
has the script and the app's side.
For full control you can instead override the scheme variables yourself via bulmaVars (e.g.
--bulma-scheme-h, --bulma-light-l, --bulma-dark-l), but colorMode covers the common case.
Color Palette Generator
At the same lightness, some hues read much lighter than others: a yellow is far lighter than a blue. So this example holds the primary's lightness low enough for its white button labels to read at every hue (see Label and Text Lightnesses).
function ColorPaletteApp() {
const [hue, setHue] = useState(171);
return (
<Theme primaryH={hue.toString()} primaryS="67%" primaryL="28%" isRoot>
<Box p="4">
<Title>Dynamic Color Palette</Title>
<input
type="range"
min="0"
max="360"
value={hue}
onChange={e => setHue(parseInt(e.target.value))}
/>
<p>Hue: {hue}°</p>
<Block display="flex" gap="2" mt="4">
<Button color="primary">Primary</Button>
<Button color="info">Info</Button>
<Button color="success">Success</Button>
<Button color="warning">Warning</Button>
<Button color="danger">Danger</Button>
</Block>
</Box>
</Theme>
);
}
Advantages Over Sass Variables
Build-time vs Runtime
/* Sass variables (build-time only) */
$primary: #ff6b35;
$family-primary: 'Helvetica Neue', sans-serif;
@import 'bulma/bulma.sass';
/* CSS variables (runtime modifiable) */
:root {
--bulma-primary-h: 18deg;
--bulma-primary-s: 100%;
--bulma-primary-l: 60%;
--bulma-family-primary: '"Helvetica Neue", sans-serif';
}
Benefits of CSS Variables
- Runtime modification: Change themes without rebuilding CSS
- User preferences: Allow users to customize appearance
- Context-aware theming: Different themes for different sections
- Performance: No need for multiple CSS bundles
- Testing: Easy to test different color schemes
- Debugging: Modify values in DevTools for instant feedback
Dark Mode & Contrast
Bulma's scheme variables (--bulma-text, --bulma-scheme-main, …) flip automatically when
data-theme="dark" is present — and when no data-theme attribute has been set at all (the
usual case for an app that never configured colorMode), they follow the visitor's OS
prefers-color-scheme. Dark mode is effectively on by default, even for designs that never
intended to support it.
That creates a silent contrast trap the moment you introduce your own fixed color tokens: on a dark-mode machine, Bulma's text goes near-white while your fixed light backgrounds stay light — white text on cream, unreadable, and invisible to you unless your own OS is in dark mode.
Apply exactly one of these rules:
Rule 1 — single-mode design: lock the scheme. If the design is light-only (or dark-only),
pin it on the <html> in your index.html, <html lang="en" data-theme="light">, so an OS
preference can never invert text out from under your palette, the first paint included.
<Theme isRoot colorMode="light"> pins it as well, but only once the app's script runs, so the
page can show the OS scheme before that.
Rule 2 — both modes: never expose a fixed token to the flip. Derive your tokens from Bulma's scheme variables, or provide the dark-mode pair yourself:
/* Preferred: your tokens track the scheme automatically. */
:root {
--my-canvas: var(--bulma-scheme-main);
--my-ink: var(--bulma-text);
}
/* Or keep custom values, but flip them for BOTH ways dark mode arrives:
the explicit attribute (colorMode="dark")… */
[data-theme='dark'] {
--my-canvas: #14251b;
--my-ink: #eef3e7;
}
/* …and the OS preference, which applies when no data-theme is set
(colorMode="system" removes the attribute): */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--my-canvas: #14251b;
--my-ink: #eef3e7;
}
}
This is exactly why deriving from the scheme variables is the preferred form — one line, and both dark-mode paths are covered automatically.
Corollary — fixed-color surfaces need fixed-color content. A surface that never flips (a
dark hero, a brand banner) must pin its content's colors too: filled buttons
(color="light", or color="primary" isInverted) and explicit text colors — not thin
outlined buttons or scheme-derived defaults, which wash out when the surrounding scheme flips.
Best Practices
Organization
Group related variables for better maintainability:
const brandTheme = {
// Brand colors
'--bulma-primary-h': '210',
'--bulma-primary-s': '100%',
'--bulma-primary-l': '50%',
// Typography
'--bulma-family-primary': '"Inter", sans-serif',
'--bulma-weight-normal': '400',
'--bulma-weight-bold': '600',
// Layout
'--bulma-radius': '8px',
'--bulma-block-spacing': '2rem',
};
Scope
- Prefer a single
isRootThemeat the top of your tree for global values. - Use nested, non-root
Themeelements only to override a specific subtree (a dashboard section, a marketing block) — each non-root theme wraps its children in a<div>, so don't nest one around a single button when a helper prop would do.
Consistency
- Establish a design system with your CSS variables
- Document which variables your team uses most commonly
- Create reusable theme objects for common scenarios
Further Reading
- Theme component reference — the full bestax React API.
- Bulma: CSS Variables — how Bulma defines and consumes CSS variables.
- Bulma: Themes — Bulma's theme system and scoping rules.
- Bulma: Dark Mode — dark-scheme conventions.
- Bulma: Customize with CSS Variables — CSS-only customization guide.