Button

Accessible native <button> with shared loading/disabled resolution, content slots, and unstyled styling hooks.

PreviewInteractive demo

Usage

tsx
import { Button } from "@sometic/react/button";

export function Example() {
    return <Button onClick={() => {}}>Save</Button>;
}
tsx
import { Button } from "@sometic/react/button";

export function Example(): JSX.Element {
    return <Button onClick={() => {}}>Save</Button>;
}
html
<script type="module">
    import { registerButtonElements } from "@sometic/elements/button";
    registerButtonElements();
</script>

<sometic-button>Save</sometic-button>

How it works

  1. Engine (@sometic/dom): resolveButton(options) builds a view model: native type, nativeDisabled (forced when loading), shouldIgnorePress, root/slot class and style maps, and state attributes (data-disabled, data-loading, optional data-size / data-variant). handleButtonPress / bindButton gate clicks while disabled or loading.
  2. Adapters: React (Button from @sometic/react/button) and Vue (@sometic/vue/button) call resolveButton each render and map slots (prefix / content / suffix / loader) onto a real <button>.
  3. DOM / custom element: sometic-button (from @sometic/elements/button) hosts a light-DOM inner <button>, reflects observed attributes, and re-resolves on change. Optional shadow opts into an open shadow root.

Behavior stays in the engine; frameworks only bind props and events.

Anatomy

Partdata-slotRole
RootrootThe <button> host
PrefixprefixLeading adornment (React prefix / Vue #prefix)
ContentcontentLabel / children
SuffixsuffixTrailing adornment
LoaderloaderPresent while loading is true

State attributes on root (from resolve): data-disabled, data-loading, optional data-size, data-variant. Loading also sets aria-busy="true".

Props / attributes

React ButtonProps

Extends native ButtonHTMLAttributes except type / disabled / prefix are owned by the engine, plus StyleableProps for slots root | prefix | content | suffix | loader.

PropTypeDefaultDescription
type"button" | "submit" | "reset""button"Native button type
disabledbooleanfalseDisables interaction
loadingbooleanfalseBusy state; forces disabled press
namestring,Form association name
valuestring,Form association value
formstring,Associated form id
sizestring,Reflected as data-size
variantstring,Reflected as data-variant
unstyledbooleanfalseSkip default class/style resolution
classesper-slot ClassValue map,Slot class names
stylesper-slot style map,Slot inline styles
cssVariablesRecord<string, string>,CSS custom properties on root
defaults{ className?, style? },Styleable defaults
variants{ className?, style? },Styleable variants
mergeClassMerger,Custom class merge
prefixReactNode,Leading slot
suffixReactNode,Trailing slot
childrenReactNode,Content slot
Native attrsremaining button HTML attrs,Forwarded to the <button> (ref supported)

Custom element (sometic-button)

Observed attributes: type, disabled, loading, size, variant, shadow. Children become the content slot. Light DOM is default.

Vue

Same engine options as React; use #prefix / #suffix slots instead of props. Emits native click (ignored while disabled/loading via the same press gate).

Events / callbacks

SurfaceEvent
ReactNative onClick (ignored while disabled/loading via handleButtonPress)
Vueclick
Custom elementNative click on the inner button
DOMbindButton(..., { onPress })

There is no separate onPress on React/Vue. Use native click handlers.

Controlled vs uncontrolled

Button has no value state. disabled / loading are always driven by props (or CE attributes). Toggle / async variants own pressed/pending state. See Toggle button and Async button.

Form participation

Native <button>: type="submit" / "reset", plus name, value, and form, participate in HTML form submit exactly like a stock button. Loading disables press so double-submit is blocked at the control.

Accessibility

  • Always a real <button>, keyboard activation and form semantics stay native.
  • loadingaria-busy="true" and nativeDisabled.
  • Prefer visible text in the content slot; icon-only actions need Icon button (aria-label required).
  • Do not replace with a div + role="button" unless you reimplement the full keyboard contract yourself.

Styling

Unstyled beyond your classes / theme defaults. Useful selectors:

  • [data-slot="root"|"prefix"|"content"|"suffix"|"loader"]
  • [data-disabled], [data-loading]
  • [data-size="…"], [data-variant="…"]
tsx
<Button unstyled classes={{ root: "btn", content: "btn__label", loader: "btn__spinner" }} loading>
    Saving
</Button>

Edge cases

  • disabled + loading, both force ignored presses; loading alone still disables.
  • Re-entrancy, rapid clicks while loading are ignored; AsyncButton also aborts prior work.
  • SSR: resolveButton is pure; register sometic-button only in the browser.
  • Multi-instance, no module singletons; each resolve/bind is independent.
  • Empty children, still a focusable button; ensure an accessible name (text or aria-label).

Performance notes

Resolve is a pure function (no observers). Adapters re-resolve on prop change only. Prefer @sometic/react/button (or Vue/elements subpaths) over barrel imports so unused icon/toggle/async code tree-shakes away. Binding one listener in bindButton avoids duplicating press-gate logic in app code.

When to use / When not

Use for primary actions, form submit/reset, and any control that must behave like a native button across frameworks.

Do not use for:

FAQ

Does loading disable the button? Yes. Loading implies disabled press handling and sets nativeDisabled.

Can I use type="submit" inside a form? Yes. Native type, name, value, and form pass through.

Why slots instead of wrapping children myself? Shared engines and custom elements need stable data-slot parts so CSS and loaders stay consistent across adapters.

Is there a visual theme baked in? No. Pair with @sometic/theme or your CSS via classes, styles, and cssVariables.

Light DOM or shadow? Light DOM is the default so page CSS reaches the control. Add shadow on the CE for isolation.

Does React forward refs? Yes, to the underlying <button>.

How do I ignore clicks while loading without checking myself? Use the component/bindButton; they call handleButtonPress for you.

Bundle tip? Import from @sometic/react/button (or matching subpath), not a mega barrel.