{}

Material theming

A widget that references the Material design tokens themes itself for free: light vs. dark, palette switches, custom user themes all flow through. A widget that hardcodes colors becomes a maintenance hole. This page is the convention for getting the first kind.

The token names

BESPA exposes the Material 3 token set as CSS custom properties on the :root element. The families you’ll reach for most often:

CSS form

Color tokens are stored as R G B triplets (no parentheses, no rgb()) so you can use them in rgb() or rgba() with any alpha you want:

/* mywidget.css */
BODY.Top .MyWidget {
    background: rgb(var(--md-sys-color-surface-container));
    color: rgb(var(--md-sys-color-on-surface));
    border: solid 1px rgb(var(--md-sys-color-outline-variant));
    border-radius: 4px;
    padding: 0.5em 1ch;
    font-family: var(--md-sys-typescale-body-medium-font);
    font-size: var(--md-sys-typescale-body-medium-size);
    line-height: var(--md-sys-typescale-body-medium-line-height);
}
BODY.Top .MyWidget.Selected {
    background: rgb(var(--md-sys-color-primary-container));
    color: rgb(var(--md-sys-color-on-primary-container));
}

Compare to the antipattern — hardcoded colors that break the moment the user picks dark mode:

/* DON'T — hard-coded colors don't recolor on theme switch. */
.MyWidget {
    background: #f5f5f5;
    color: #111;
    border: solid 1px #ddd;
}

Rule of thumb: if your CSS contains a hex color or a literal rgb(123, …), you’re hard-coding the theme. Replace it with the matching token.

The BODY.Top prefix

You’ll notice every framework stylesheet prefixes its selectors with BODY.Top. This isn’t decoration — it’s a specificity trick so the framework styles outrank generic CSS from third-party libraries that share class names. Use the same prefix in your own widget CSS for the same reason.

Canvas-rendered widgets

For widgets that render via JavaScript canvas (charts, custom drawing), CSS var() inside Canvas colors doesn’t work — the canvas API doesn’t resolve CSS variables. Resolve them in JS first:

// chart/chart.js — resolves CSS var() to RGB at draw time for canvas use.
function chart_color(varName, alpha) {
    const v = getComputedStyle(document.documentElement)
        .getPropertyValue(varName).trim();
    if (!v) return null;
    const parts = v.split(/[\s,]+/);
    if (typeof alpha === "number" && alpha < 1) {
        return "rgba(" + parts[0] + "," + parts[1] + "," + parts[2] + "," + alpha + ")";
    }
    return "rgb(" + parts[0] + "," + parts[1] + "," + parts[2] + ")";
}

The chart widget does exactly this — see chart/chart.js. The pattern: read the computed CSS value, parse, hand the literal RGB to the renderer. Cache the result keyed by token name so you only pay the lookup once per token per page.

Reacting to theme changes

CSS-themed widgets recolor automatically because CSS variables flow down on every recompute. Canvas widgets need a hook — listen for class changes on the <html> element and for prefers-color-scheme media changes, clear your resolved-color cache, and re-render:

// Re-resolve and re-render when the page theme changes.
function onThemeChange() {
    // Invalidate any cached resolved colors and re-render every live instance.
    // ...
}

// Class changes on <html> — flipping LightTheme / DarkTheme.
new MutationObserver(onThemeChange).observe(document.documentElement, {
    attributes: true,
    attributeFilter: ["class"],
});

// OS-level color scheme changes when the page is in default (system-follows) mode.
window.matchMedia("(prefers-color-scheme: dark)").addEventListener(
    "change", onThemeChange,
);

This is the pattern the chart widget uses in production — see chart/chart.js for the full implementation including the instance registry.

Tonal palettes

For widgets that need many distinct colors (charts, color-coded badges, multi-series visuals), the framework also exposes --md-sys-color-primary-{0..330}deg at 30° hue rotations of the primary tone. Picking your series colors from these gives you a harmonized rainbow that retracks any palette switch.

See also

Build → Theming — the consumer-side controls (light/dark, palette).

Assets & CSS — where the widget’s CSS lives and how it gets to the browser.

Read css/keycolors.go for the palette generation and chart/chart.js for the canvas-side theme handling.