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.
Get the code
<script src="/assets/bmx-components.min.js"></script> <bmx-button tone="primary">Primary</bmx-button> <bmx-button tone="neutral">Neutral</bmx-button> <bmx-button tone="success">Success</bmx-button> <bmx-button tone="warning">Warning</bmx-button> <bmx-button tone="danger">Danger</bmx-button> <bmx-button tone="info">Info</bmx-button> <bmx-button tone="accent">Accent</bmx-button> <bmx-button variant="soft" tone="primary">Primary</bmx-button> <!-- …and 13 more like these -->
Sizes & shapes
Five steps, in rem, so the whole library scales with the reader's browser font-size setting.
Get the code
<script src="/assets/bmx-components.min.js"></script> <bmx-button size="xs">Extra small</bmx-button> <bmx-button size="sm">Small</bmx-button> <bmx-button size="md">Medium</bmx-button> <bmx-button size="lg">Large</bmx-button> <bmx-button size="xl">Extra large</bmx-button> <bmx-button shape="rounded">Rounded</bmx-button> <bmx-button shape="pill">Pill</bmx-button> <bmx-button shape="sharp">Sharp</bmx-button> <!-- …and 1 more like these -->
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.
Get the code
<script src="/assets/bmx-components.min.js"></script> <bmx-button> <svg slot="start" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" /></svg> Download </bmx-button> <bmx-button variant="outline"> Next <svg slot="end" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7" /></svg> </bmx-button> <bmx-button variant="soft" tone="neutral" icon-only="true" label="Settings"> <svg slot="start" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9v.09a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg> </bmx-button> <bmx-button badge="12" variant="outline" tone="neutral"> <svg slot="start" viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 0 1-3.46 0" /></svg> Notifications </bmx-button>
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.
Get the code
<script src="/assets/bmx-components.min.js"></script> <bmx-button loading="true">Saving</bmx-button> <bmx-button loading="true" loading-label="replace" loading-text="Uploading…" variant="soft">Upload</bmx-button> <bmx-button toggle="true">Toggle me</bmx-button> <bmx-button toggle="true" pressed="true" variant="soft">Pressed</bmx-button> <bmx-button disabled="true">Disabled</bmx-button> <bmx-button disabled="true" focusable-when-disabled="true" described-by="why-disabled" variant="outline"> Disabled, still reachable </bmx-button>
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.
Get the code
<script src="/assets/bmx-components.min.js"></script> <bmx-button type="submit" name="intent" value="subscribe">Subscribe</bmx-button> <bmx-button type="reset" variant="ghost" tone="neutral">Reset</bmx-button>
Links & responsive
A button with href renders a real anchor, with link keyboard semantics: Enter activates, Space does not.
Get the code
<script src="/assets/bmx-components.min.js"></script> <bmx-button href="https://binarymission.co.uk" target="_blank"> Binarymission <svg slot="end" viewBox="0 0 24 24"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14 21 3" /></svg> </bmx-button> <bmx-button href="#buttons" variant="link">Jump to variants</bmx-button> <bmx-button hide-label-below="md" variant="outline" tone="neutral"> <svg slot="start" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" /></svg> Export </bmx-button> <bmx-button hide-label-below="md" variant="outline" tone="neutral"> <svg slot="start" viewBox="0 0 24 24"><path d="M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 14h12v8H6z" /></svg> Print </bmx-button> <bmx-button hide-label-below="md" variant="outline" tone="neutral"> <svg slot="start" viewBox="0 0 24 24"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13" /></svg> Share </bmx-button> <bmx-button full-width="true" variant="soft">Full width</bmx-button>
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.