Theming
Everything BMX paints comes from CSS custom properties defined on :root.
Custom properties inherit through shadow boundaries, so overriding one in your
own stylesheet retints every component — with no build step, no ::part
gymnastics, and no theme compiler.
The three levels
1. Pick a theme. Ten are built in: light, dark, ocean, sunset,
aurora, royal, mint, forest, slate, amber.
<html data-theme="ocean"> <!-- whole page -->
<div data-bmx-theme="ocean">…</div> <!-- or one panel, scoped -->
The scoped attribute wins, because a page that wants one dark panel on a light
site should not have to fight for it. data-theme matches the convention
binarymission.co.uk already uses, so a BMX component dropped onto that site
picks up the visitor's chosen theme with no wiring.
2. Override the seeds. Each theme sets seven seed colours and a handful of surfaces. Every state colour — hover, active, soft, outline, focus ring — is derived from those, so changing a seed changes forty-nine derived values consistently:
:root {
--bmx-primary: #1d4ed8;
--bmx-danger: #b91c1c;
--bmx-radius-md: 2px;
--bmx-font-family: 'Your Brand Sans', system-ui, sans-serif;
}
3. Override one instance. Every component exposes its own geometry:
bmx-button.cta {
--bmx-button-height: 3.25rem;
--bmx-button-padding-inline: 2rem;
--bmx-button-radius: 999px;
}
And its internals through ::part, for the cases tokens do not reach:
bmx-button::part(base) { letter-spacing: 0.06em; }
bmx-button::part(badge) { background: rebeccapurple; }
How derivation works
--bmx-state-mix is the direction a hover or active state moves in: black on a
light theme, white on a dark one. One rule then serves both:
--bmx-primary-solid-hover: color-mix(in oklab, var(--bmx-primary) 86%, var(--bmx-state-mix));
Mixing happens in oklab, not sRGB. Mixing towards black in sRGB desaturates as it darkens, turning a confident primary blue into a muddy navy on hover; oklab moves the lightness and leaves the hue alone, which is what "the same colour, pressed" should look like.
Density
<div data-bmx-density="compact">…</div>
compact (0.875×), comfortable (default) and spacious (1.125×) scale every
control's height and padding without touching its font size — which is what a
data-dense enterprise screen actually wants.
Adding a theme
Ten declarations, not four hundred:
[data-bmx-theme='midnight'] {
color-scheme: dark;
--bmx-state-mix: #fff;
--bmx-tint: #000;
--bmx-surface: #05070f;
--bmx-surface-sunken: #03040a;
--bmx-surface-raised: #0d1220;
--bmx-surface-overlay: #101728;
--bmx-text: #e8ecf7;
--bmx-text-muted: #9aa5bd;
--bmx-text-subtle: #6f7a93;
--bmx-border: #1b2338;
--bmx-border-strong: #2c3757;
--bmx-primary: #6ea8ff;
--bmx-neutral: #8390a8;
--bmx-success: #34d399;
--bmx-warning: #fbbf24;
--bmx-danger: #f87171;
--bmx-info: #38bdf8;
--bmx-accent: #a78bfa;
--bmx-shadow-color: 225 60% 3%;
}
Click effects
bmx-button can paint an effect from the point of activation. Seven of them,
off by default — a page where every button animates is a page where none of them
mean anything.
ripple |
What it does |
|---|---|
none |
Nothing. The default |
wave |
One circle expanding from the pointer. The familiar Material ripple |
rings |
Three concentric rings radiating outward, staggered |
spiral |
An Archimedean spiral, drawn outward from the centre as it rotates |
burst |
Eight spokes firing outward |
glow |
A soft radial bloom with no hard edge. The quietest of the set |
sweep |
A diagonal highlight running across the whole control |
echo |
A pulse tracing the control's own outline |
<bmx-button ripple="spiral">Generate</bmx-button>
Tune any of them from a stylesheet — there is no API surface to thread through your components:
bmx-button {
--bmx-effect-duration: 900ms;
--bmx-effect-color: #22d3ee; /* defaults to the button's text colour */
--bmx-effect-opacity: 0.6;
--bmx-effect-easing: cubic-bezier(0.16, 1, 0.3, 1);
}
Three things worth knowing:
- Keyboard activation plays them too, from the centre. A keyboard user pressing Enter gets the same feedback a mouse user gets; omitting it is the sort of asymmetry nobody notices until they stop using the mouse.
- Nothing paints under
prefers-reduced-motion. Not a zeroed duration — which would be a flash — but no element at all. - Every effect is
transformandopacityonly. None touches a property that can trigger layout, so a screen full of animating buttons stays at 60fps.
sweep and echo follow the control's own bounds rather than the pointer; the
other five originate wherever you pressed.
The neutral tone is deliberately different
Every other tone derives its solid fill from its seed. Neutral does not: its solid is a raised surface with a real border.
Two reasons. A solid mid-grey slab is what makes a dark theme look unfinished; and neutral is the tone a secondary action uses, so a neutral button carrying as much visual weight as the primary one beside it defeats the point of having two. The seed still drives neutral's soft, outline, ghost and focus-ring treatments, where a grey is exactly right.