VESYL UI

Action status

Transient feedback for async work — the same feel as a copy button’s green check, but for pending → success / error → idle.

  • One-shot buttons (labeled or icon-only): prefer composed ActionButton.
  • Disclosures / metric strips / custom chrome: use useActionStatus + ActionStatusIcon here.

useActionStatus owns the state machine and timers. ActionStatusIcon (returned as save.icon) swaps the trailing indicator with a short pop-in animation. Defaults: disclosure chevron while idle, spinner while pending, green check on success, red X on error.

Instant work still spends at least 350ms in pending (minPendingMs) so the spinner → check transition doesn’t feel like a flash. Success color is set on the status wrapper so parent disclosure styles can’t wash out the green.

import { useActionStatus, Button } from "@vesyl/ui-next";

const save = useActionStatus();

await save.run(async () => {
  await mutate(); // throw or reject → error status, then rewind
});

Save button

Pass a custom idle icon (here a save glyph). The returned save.icon cycles through pending / success / idle. Disable the control while save.isBusy to prevent double-submit.

Disclosure chevron

Default idle icon is a chevron — drop save.icon into DisclosureButton (or RegionDisclosure.Value) and set rotateOnOpen={save.status === "idle"} so the spinner and check don’t flip when the popover is open.

Simulate a packaging save: open the popover, hit Save, watch the trailing icon.

Region disclosure (metric strip)

Same pattern on a parcel-style metric row. Each cell owns its own useActionStatus so box and weight feedback stay independent.

Parcel metrics

Error path

If run’s promise rejects (or you call fail() / settle(false)), the icon shows the error state for errorMs, then returns to idle. Pair with a toast for the message — the icon is only the ambient signal.

status: idle

Halo

Set variant="halo" to keep one Halo mounted while the status changes. Pending uses a neutral gray spinner; success and error transition the Halo color while swapping to circular status icons. Provide a circular idle icon to keep the Halo visible between actions, or null to show it only while an action is active.

Custom status icons

Override any of pending / success / error (and the idle icon) when the control isn’t a disclosure — e.g. a delete mini-button.

Reference

useActionStatus(options?)

OptionTypeDefaultDescription
successMsnumber1400How long to hold the success icon before idle
errorMsnumber1800How long to hold the error icon before idle
minPendingMsnumber350Minimum time in pending so instant work still reads as “working”
iconReactNode | nullchevronIdle icon. undefined → disclosure chevron; null → empty idle slot
icons{ pending?, success?, error? }Override status icons
variant"inline" | "halo""inline"Compact indicator or transitioning Halo presentation
size"sm" | "md" | "lg""sm" for HaloHalo size; ignored by the inline variant
colorsPartial<Record<ActionStatus, CSS color>>Override the color used for any state
ReturnTypeDescription
status'idle' | 'pending' | 'success' | 'error'Current state
isBusybooleantrue while pending
iconReactNodeReady-to-render trailing indicator
run(fn)<T>(fn: () => Promise<T>) => Promise<T | undefined>Wraps async work; rejects → error status, returns undefined (no rethrow)
settle(ok)(ok: boolean) => voidtrue → success, false → error (for toastMutationResult-style booleans)
start / succeed / fail / reset() => voidManual control if you already own the async flow

ActionStatusIcon

Presentational only — useful if you drive status yourself.

PropTypeDefaultDescription
statusActionStatusWhich icon to show
iconReactNode | nullchevronIdle icon (null = empty)
iconsActionStatusIconsOverrides for pending / success / error
variant"inline" | "halo""inline"Compact indicator or transitioning Halo presentation
size"sm" | "md" | "lg""sm" for HaloHalo size; ignored by the inline variant
colorsActionStatusColorsOverrides state colors
classNamestringMerged onto the wrapper

With disclosures

const save = useActionStatus();

<DisclosureButton
  icon={save.icon}
  rotateOnOpen={save.status === "idle"}
>
  Ship weight
</DisclosureButton>

// Full-bleed metric cells:
<RegionDisclosure.Value
  icon={save.icon}
  rotateOnOpen={save.status === "idle"}
>
  Small Box
</RegionDisclosure.Value>

RegionDisclosure.Value and DisclosureButton both accept icon and rotateOnOpen so status feedback drops in without a custom layout.