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+ActionStatusIconhere.
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.
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.
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?)
| Option | Type | Default | Description |
|---|---|---|---|
successMs | number | 1400 | How long to hold the success icon before idle |
errorMs | number | 1800 | How long to hold the error icon before idle |
minPendingMs | number | 350 | Minimum time in pending so instant work still reads as “working” |
icon | ReactNode | null | chevron | Idle 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 Halo | Halo size; ignored by the inline variant |
colors | Partial<Record<ActionStatus, CSS color>> | — | Override the color used for any state |
| Return | Type | Description |
|---|---|---|
status | 'idle' | 'pending' | 'success' | 'error' | Current state |
isBusy | boolean | true while pending |
icon | ReactNode | Ready-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) => void | true → success, false → error (for toastMutationResult-style booleans) |
start / succeed / fail / reset | () => void | Manual control if you already own the async flow |
ActionStatusIcon
Presentational only — useful if you drive status yourself.
| Prop | Type | Default | Description |
|---|---|---|---|
status | ActionStatus | — | Which icon to show |
icon | ReactNode | null | chevron | Idle icon (null = empty) |
icons | ActionStatusIcons | — | Overrides for pending / success / error |
variant | "inline" | "halo" | "inline" | Compact indicator or transitioning Halo presentation |
size | "sm" | "md" | "lg" | "sm" for Halo | Halo size; ignored by the inline variant |
colors | ActionStatusColors | — | Overrides state colors |
className | string | — | Merged 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.