Getting started
Zero npm dependencies. Native browser rendering. Nothing to break when your framework upgrades. BMX is one self-contained JavaScript file with no packages to install, audit or keep patched. Every control is a standard custom element that the browser itself renders, so it does not sit on top of React, Angular or Vue internals — upgrading your framework, or replacing it, leaves the controls untouched.
That is why they behave the same in plain HTML, React, Angular, Vue, Svelte,
Blazor, ASP.NET MVC, or a twelve-year-old server-rendered application with no
build step at all. The runtime makes no network calls, uses no eval, and runs
under a strict Content Security Policy (script-src 'self').
The one-file route
Copy bmx-components.min.js next to your pages and add one tag:
<script src="/assets/bmx-components.min.js"></script>
<bmx-button tone="primary">Save</bmx-button>
That is the whole installation. The runtime registers every <bmx-*> element
and injects its own design tokens, so there is no second stylesheet to
remember and nothing to configure.
Elements upgrade whenever they appear, so markup rendered later — by a templating engine, an htmx swap, a jQuery plugin — works without being told.
The bundler route
If your application already has a build step:
import { defineCustomElements } from 'bmx-webcomponents/loader';
import 'bmx-webcomponents/dist/bmx-tokens.css';
defineCustomElements();
Components are lazy-loaded on this route: an application that uses two of them downloads two, not twenty.
Attributes and properties
Simple values go through attributes, exactly as they look:
<bmx-button variant="outline" tone="danger" size="lg">Delete</bmx-button>
Anything that is not a string is a property, because an HTML attribute can
only ever be a string — an array pushed through one arrives as
"[object Object]", which is the single most common integration problem with
any Web Component:
document.querySelector('bmx-split-button').items = [
{ id: 'save-as', label: 'Save as…' },
{ separator: true },
{ id: 'delete', label: 'Delete', danger: true },
];
The generated framework wrappers do this for you.
Events
Every component's events are CustomEvents carrying a detail:
button.addEventListener('bmxActivate', event => {
console.log(event.detail.via); // 'pointer' | 'keyboard' | 'hold' | 'programmatic'
});
bmx-button also lets the ordinary click event through, so existing code
keeps working. It is suppressed only when one of the activation guards
(confirm, hold, cooldown) has decided the click does not count — which is
what makes confirm work with no code at all.
Frameworks
React
import { BmxButton } from 'bmx-webcomponents-react';
<BmxButton tone="danger" confirm="Click again to delete" onBmxActivate={remove}>
Delete
</BmxButton>
Angular
<bmx-button tone="danger" [confirm]="'Click again'" (bmxActivate)="remove()">
Delete
</bmx-button>
Vue
<BmxButton tone="danger" confirm="Click again" @bmxActivate="remove">Delete</BmxButton>
The wrappers contain no component code. They render the tag and bridge properties and events, which is why one wrapper serves both the demo and the commercial runtime.
Server-side rendering
The components guard every document and window access, so importing them
in a Node render pass is safe. They render on the client; nothing throws on the
server.
Security reviews
For a Content Security Policy, the runtime needs only script-src 'self' — no
'unsafe-eval'. Components set inline styles for sizing and positioning, so
style-src needs 'self' 'unsafe-inline'. The file contacts no server of its
own; the only third-party code inside it is the small MIT-licensed Stencil
runtime, declared in THIRD-PARTY-NOTICES.md.