VESYL UI

Action button

ActionButton is Button + action-status for one-shot async work that stays on screen. Idle glyph → spinner (min pending) → green check or red X → idle.

Use it for header / settings actions (sync, refresh, enqueue). Prefer raw useActionStatus for disclosures and metric strips. Prefer Button loading / Form.SubmitButton for form submit and dialogs that close on success.

import { ActionButton } from "@vesyl/ui-next";
import { RefreshCcwIcon } from "lucide-react";

<ActionButton
  variant="outline"
  icon={<RefreshCcwIcon />}
  action={async () => {
    const ok = toastMutationResult({ /* errors only — no successMessage */ });
    if (!ok) throw new Error("failed");
  }}
>
  Sync orders
</ActionButton>

Contract: throw / reject on failure so the error face shows. Toast the error message in the caller (or onError). Do not toast success — the check is the signal.

Labeled

Status face defaults to the start (leading). Pass iconPosition="end" for a trailing face.

Icon-only

Omit children; pass aria-label. Same lifecycle as labeled.

Error path

Failed action → red X (+ optional toast). The icon is ambient; the toast carries the message.

External triggers (ref)

When a menu or second control should drive the same face (e.g. multi-store sync):

const ref = useRef<ActionButtonHandle>(null);

<ActionButton ref={ref} icon={} action={() => work(preferred)} />

// Dropdown item:
void ref.current?.run(() => work(otherStore));

onStatusChange can lift pending to disable sibling controls.

vs other patterns

PatternUse when
ActionButtonOne-shot command; button remains; ambient ✓/✕
useActionStatus aloneDisclosure chevrons, multi-cell layouts, custom chrome
Button loading / Form.SubmitButtonForm submit; dialog primary that closes/navigates on success

Reference

ActionButton

Accepts all Button props except loading and onClick (click is owned by action).

PropTypeDefaultDescription
iconReactNodeIdle face (required)
action() => Promise<unknown>Default click work; throw → error
iconPosition'start' | 'end''start'Status face when labeled
successMs / errorMs / minPendingMsnumbersame as action-statusTiming
icons / labelsStatus icon / live-region overrides
onError(error) => voidAfter catch (e.g. report); does not rethrow
onStatusChange(status) => voidEvery status transition

ActionButtonHandle (ref)

FieldDescription
run(fn)Same as useActionStatus().run
isBusystatus === 'pending'
statusCurrent ActionStatus