<bmx-button>
The library's workhorse, and the component every other one borrows its
conventions from: variant for shape of treatment, tone for meaning,
size for the scale step, states expressed as reflected attributes so the
stylesheet does the branching rather than the TypeScript.
36 properties · 6 events · 5 methods · 8 parts
Example
Show markup
<div class="row">
<bmx-button tone="primary">Solid</bmx-button>
<bmx-button variant="soft" tone="primary">Soft</bmx-button>
<bmx-button variant="outline" tone="neutral">Outline</bmx-button>
<bmx-button variant="ghost" tone="neutral">Ghost</bmx-button>
<bmx-button variant="link" tone="primary">Link</bmx-button>
</div>
<div class="row">
<bmx-button tone="success" ripple="rings">
<svg slot="start" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
Approve
</bmx-button>
<bmx-button tone="danger" confirm="Click again to delete">Delete</bmx-button>
<bmx-button tone="danger" variant="outline" hold="1500">Hold to wipe</bmx-button>
<bmx-button variant="soft" tone="neutral" icon-only="true" label="Settings" ripple="glow">
<svg slot="start" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/></svg>
</bmx-button>
<bmx-button badge="7" variant="outline" tone="neutral">Inbox</bmx-button>
</div>
WHY A CUSTOM ELEMENT RATHER THAN A CSS CLASS
Most of what makes a button hard is not its appearance. It is form submission from inside a shadow root, the accessible name of an icon-only button, the difference between Enter and Space on a link, keeping a destructive action from firing on a stray double-click, and not letting an async handler be re-entered while it is still running. A class gives a consumer none of that; this gives them all of it in one tag.
ACCESSIBILITY
- Renders a real
<button>or a real<a>, so the platform supplies the role, the keyboard behaviour and the form participation rather than us reimplementing them withrole="button"and akeydownhandler. focusableWhenDisabledoffers WCAG 2.2's preferred treatment for a disabled control in a toolbar:aria-disabledrather thandisabled, so the control keeps its Tab stop and can still be discovered and described.- The busy state sets
aria-busyand anaria-liveregion announces the change once, rather than on every render. - Icon-only buttons refuse to render without an accessible name:
labelis required, and a console warning names the offending element in development. - Hit target reaches 24x24 CSS pixels at every size step (SC 2.5.8), and larger again on a coarse pointer.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
action |
property only | (event: BmxButtonActivateDetail) => unknown | Promise<unknown> |
— | An async handler that drives the busy state. A property, not an attribute. Set it and the button becomes busy for the lifetime of the returned promise, refuses re-entry while it runs, and clears itself on both resolve and reject - which is the half everyone forgets, leaving a spinner on screen after a failed save. |
autoFocus |
auto-focus |
boolean |
false |
Take focus on first render. |
badge |
badge |
string | number |
— | A small counter or status badge rendered on the button's trailing corner. |
confirm |
confirm |
string |
— | Turn the button into a two-step confirmation. The first activation arms it and swaps the label for this text; the second fires. Cheaper than a modal for a destructive action on a row, and - unlike window.confirm - it does not steal focus or block the main thread. |
confirmTimeout |
confirm-timeout |
number |
3000 |
How long an armed button waits before disarming, in ms. Floored at 1000. |
controls |
controls |
string |
— | aria-controls: the id of the element this button controls. |
cooldown |
cooldown |
number |
0 |
Minimum gap between two activations, in ms. Guards against double-submit. |
describedBy |
described-by |
string |
— | aria-describedby: the id of an element that explains this button. |
disabled |
disabled |
boolean |
false |
Disable the button. |
download |
download |
string |
— | Anchor download attribute. |
expanded |
expanded |
boolean |
— | aria-expanded, for a disclosure or menu trigger. |
focusableWhenDisabled |
focusable-when-disabled |
boolean |
false |
Keep a disabled button focusable, marking it aria-disabled instead. WCAG 2.2 prefers this for controls inside a toolbar or a group: a natively disabled control vanishes from the Tab order, so a keyboard user never learns it exists, let alone why it is unavailable. Pair it with describedBy pointing at the explanation. |
fullWidth |
full-width |
boolean |
false |
Stretch to the width of the container. |
haspopup |
haspopup |
'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' |
— | aria-haspopup, for a button that opens a menu, dialog or listbox. |
hideLabelBelow |
hide-label-below |
BmxBreakpoint |
— | Drop the visible label below this breakpoint, keeping the icon. The responsive escape hatch for toolbars: a row of five labelled actions that would wrap on a phone becomes a row of five icons instead. The label stays in the accessibility tree, so nothing is lost to a screen reader. |
hold |
hold |
number |
0 |
Require the button to be held for this many milliseconds. For the genuinely irreversible action. A progress track fills as the user holds, which makes the requirement discoverable rather than mysterious. Works from the keyboard too: hold Enter or Space. |
href |
href |
string |
— | Render as an anchor. Sets link keyboard semantics: Enter activates, Space does not. |
iconOnly |
icon-only |
boolean |
false |
Render as icon-only. Usually inferred - a button with slotted icons and no label is icon-only without being told - but settable for the case where the label exists and should be visually hidden while remaining the accessible name. |
justify |
justify |
BmxJustify |
'center' |
How content is packed when the button is wider than its content. |
label |
label |
string |
— | The accessible name. Required when the button is icon-only. When a visible label is present this overrides it for assistive technology, which is occasionally what you want ("Delete" on screen, "Delete invoice INV-2451" announced). |
loading |
loading |
boolean |
false |
Show the busy state and block activation. |
loadingLabel |
loading-label |
'replace' | 'keep' |
'keep' |
Whether the busy state replaces the label or sits beside it. |
loadingText |
loading-text |
string |
'Working…' |
Label announced and optionally shown while busy. |
name |
name |
string |
— | Submitted name, when this button is a form's submitter. |
pressed |
pressed |
boolean |
false |
The toggle state. Ignored unless toggle is set. |
rel |
rel |
string |
— | Anchor rel. Merged with the automatic noopener noreferrer for _blank. |
ripple |
ripple |
BmxRipple |
'none' |
The effect painted from the point of activation. wave is the familiar Material ripple; rings sends concentric rings outward; spiral unwinds an Archimedean spiral; burst fires spokes; glow blooms softly; sweep runs a highlight across the control; echo pulses a ring out past its edge. Tune any of them per button, or globally, without touching the component: css bmx-button { --bmx-effect-duration: 900ms; --bmx-effect-color: #fff; --bmx-effect-opacity: 0.5; --bmx-effect-easing: cubic-bezier(0.16, 1, 0.3, 1); } Off by default: a flourish should be opted into, and a page where every button animates is a page where none of them mean anything. Suppressed entirely under prefers-reduced-motion, and never rendered to assistive technology - it is decoration, and decoration only. |
rovingTabindex |
roving-tabindex |
number |
— | The tabindex applied to the rendered control. Managed by a parent bmx-button-group (or any other composite that implements a roving tabindex) and not normally set by hand. It exists as a property because the focusable element lives inside this component's shadow root, where a parent cannot reach it - and a composite widget that cannot take its children out of the tab order cannot implement the WAI-ARIA keyboard pattern at all. |
shape |
shape |
BmxShape |
'rounded' |
Corner treatment. circle is intended for icon-only buttons. |
size |
size |
BmxSize |
'md' |
Size step. |
target |
target |
'_self' | '_blank' | '_parent' | '_top' |
— | Anchor target. rel is hardened automatically for _blank. |
toggle |
toggle |
boolean |
false |
Behave as a toggle, exposing aria-pressed. |
tone |
tone |
BmxTone |
'primary' |
Semantic colour role. |
type |
type |
'button' | 'submit' | 'reset' |
'button' |
Native button type. Ignored in link mode. |
value |
value |
string |
— | Submitted value, paired with name. |
variant |
variant |
BmxButtonVariant |
'solid' |
Visual treatment. |
Events
| Event | Detail | Description |
|---|---|---|
bmxActivate |
BmxButtonActivateDetail |
Fired when the button actually activates - after every guard has passed. The native click event still fires and still bubbles, so existing code keeps working; it is simply suppressed when a guard blocks or arms the button, which is the behaviour a consumer expects from confirm without having to write anything. |
bmxBusyChange |
boolean |
Fired when the busy state changes, in either direction. |
bmxConfirmArm |
void |
Fired when a confirming button arms itself, awaiting a second activation. |
bmxConfirmCancel |
void |
Fired when an armed button disarms without activating. |
bmxHoldProgress |
number |
Fired repeatedly while a press-and-hold is in progress, with a 0-1 fraction. |
bmxPressedChange |
boolean |
Fired when the toggle state changes. |
Methods
| Method | Signature | Description |
|---|---|---|
activate |
activate() => Promise<boolean> |
Activate the button programmatically, bypassing pointer and keyboard. Guards still apply: a programmatic activation of a confirming button arms it exactly as a click would. Anything else would make confirm a decoration rather than a guarantee. |
removeFocus |
removeFocus() => Promise<void> |
Remove focus from the button. |
reset |
reset() => Promise<void> |
Disarm a confirming button without activating it. |
setBusy |
setBusy(value: boolean) => Promise<void> |
Set the busy state by hand, for a flow the action property cannot express. |
setFocus |
setFocus(options?: FocusOptions) => Promise<void> |
Move focus to the button. |
Slots
| Slot | Description |
|---|---|
(default) |
The button's label. |
end |
Content after the label, typically an icon or chevron. |
loading |
Replaces the built-in spinner while busy. |
start |
Content before the label, typically an icon. |
CSS shadow parts
| Part | Description |
|---|---|
badge |
The counter badge. |
base |
The rendered <button> or <a>. |
end |
The trailing slot wrapper. |
hold |
The press-and-hold progress track. |
label |
The label wrapper. |
ripple |
A ripple instance. |
spinner |
The busy indicator. |
start |
The leading slot wrapper. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-button-border-width |
Border width, for the outline variant. |
--bmx-button-font-size |
Label font size. |
--bmx-button-font-weight |
Label font weight. |
--bmx-button-gap |
Space between the icon and the label. |
--bmx-button-height |
The control's height. Defaults to the size step's height times the density scale. |
--bmx-button-icon-padding |
Horizontal padding of an icon-only button. Defaults to none. |
--bmx-button-icon-size |
Size of a slotted icon. |
--bmx-button-icon-width |
Width of an icon-only button. Defaults to its height, making it square. |
--bmx-button-padding-inline |
Horizontal padding. |
--bmx-button-radius |
Corner radius. Accepts the full four-value form, which is how a button group rounds only its outer corners. |
--bmx-effect-color |
What the click effect paints in. Defaults to the button's own text colour. |
--bmx-effect-duration |
How long the click effect runs. |
--bmx-effect-easing |
The click effect's timing curve. |
--bmx-effect-opacity |
The click effect's strength at its peak. |