Playground
Every property, live. The generated markup below is exactly what you would paste into your own application.
Variants
Six treatments. variant decides the shape of the treatment; tone decides its meaning.
<bmx-button variant="solid">Solid</bmx-button> <bmx-button variant="elevated">Elevated</bmx-button> <bmx-button variant="soft">Soft</bmx-button> <bmx-button variant="outline">Outline</bmx-button> <bmx-button variant="ghost">Ghost</bmx-button> <bmx-button variant="link">Link</bmx-button>
Tones
Chosen by meaning, not colour. A delete button is danger whatever the active theme decides danger looks like.
Sizes & shapes
Five steps, in rem, so the whole library scales with the reader's browser font-size setting.
Icons
Slotted, not bundled — so BMX ships no icon licence and you keep the icon set your design system already uses.
An icon-only button without a label logs a console warning in development. A button nobody can name is a
button a screen-reader user cannot use — and it looks perfectly fine to everyone else, which is why it survives to production.
Click effects
Seven of them, radiating from wherever you pressed — and from the centre when you activate with the keyboard.
Click the buttons below. Every one is transform and opacity only, so a screen full of them
still runs at 60fps, and all seven are suppressed entirely under prefers-reduced-motion.
<bmx-button ripple="spiral">Spiral</bmx-button>
<!-- Tuned from a stylesheet. No API surface, no props to thread through. -->
<style>
bmx-button {
--bmx-effect-duration: 900ms;
--bmx-effect-color: #22d3ee;
--bmx-effect-opacity: 0.6;
--bmx-effect-easing: cubic-bezier(0.16, 1, 0.3, 1);
}
</style>
The spiral is a real Archimedean spiral, sampled once at module load and drawn with stroke-dashoffset so the
line is drawn outward from the centre rather than fading in whole. Its geometry is unit tested — the shape is code, not a
hand-tuned SVG someone will be frightened to touch.
Guarded actions
The part every application eventually hand-rolls, badly. One attribute each.
<bmx-button tone="danger" confirm="Click again to delete">Delete account</bmx-button>
<bmx-button tone="danger" hold="1500">Hold to wipe data</bmx-button>
<bmx-button cooldown="3000">Send code</bmx-button>
<script>
// `action` is a property, not an attribute: the button becomes busy for the
// lifetime of the promise and clears itself on both resolve and reject.
document.querySelector('bmx-button').action = () => fetch('/api/save', { method: 'POST' });
</script>
States
Busy, toggled, disabled — and the WCAG 2.2 treatment of a disabled control that stays discoverable.
The last button keeps its Tab stop and is marked aria-disabled instead of disabled. A natively
disabled control vanishes from the Tab order, so a keyboard user never learns it exists — let alone why it is unavailable.
This paragraph is its aria-describedby.
Forms
Real form participation from inside a shadow root, via ElementInternals. Constraint validation runs; the submit event is cancellable.
Try submitting it empty — the browser's own validation message appears, because the button calls
requestSubmit() rather than submit(). Most component libraries get this wrong and skip validation entirely.
Links & responsive
A button with href renders a real anchor, with link keyboard semantics: Enter activates, Space does not.
Buttons that act together
Groups carries the two components built out of this one: a button group that turns several buttons into one widget with a single Tab stop, and a split button that joins a default action to a menu of alternatives. Everything on this page — the tones, the sizes, the guards — works inside both of them unchanged, which is the point of them being the same element.