v1.0.0

<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

Solid Soft Outline Ghost Link
Approve Delete Hold to wipe Inbox
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 with role="button" and a keydown handler.
  • focusableWhenDisabled offers WCAG 2.2's preferred treatment for a disabled control in a toolbar: aria-disabled rather than disabled, so the control keeps its Tab stop and can still be discovered and described.
  • The busy state sets aria-busy and an aria-live region announces the change once, rather than on every render.
  • Icon-only buttons refuse to render without an accessible name: label is 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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

SlotDescription
(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

PartDescription
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

PropertyDescription
--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.