v1.0.0

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 transform and opacity only. 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.