Action ButtonNew

An async-aware button with an idle → pending → success/error state machine. Composes over Button, adding promise tracking, abort support, minimum-pending opt-in, auto-reset, layered crossfade transitions, and live screen-reader announcements.

Dependencies

motion@radix-ui/react-slotclass-variance-authority

Interaction Type

Click triggers the async onAction callback. State transitions crossfade with opacity + scale(0.95) + 1px blur — one morph moment. Focus is never moved. Width is stabilized by an invisible sizing layer. Pending state disables interaction and announces via live region. Success/error auto-reset to idle after a configurable delay.

Props

Options you can pass to customize this component.

Prop
Type
Description
state
"idle" | "pending" | "success" | "error"

Controlled state. Overrides internal state machine.

defaultState
"idle" | "pending" | "success" | "error"

Default state for uncontrolled mode. Defaults to "idle".

onAction
(signal: AbortSignal) => Promise<void>

Async callback triggered on click. The component tracks the returned promise and transitions through pending → success/error automatically.

onStateChange
(state: ActionButtonState) => void

Called whenever the internal state changes.

minPendingMs
number

Minimum milliseconds to display pending state. Prevents flicker for fast operations. Defaults to 0 (no minimum).

resetDelayMs
number

Milliseconds before resetting from success/error to idle. Defaults to 2000. Set to 0 to disable.

idleLabel
ReactNode

Content shown in idle state.

pendingLabel
ReactNode

Content shown in pending state.

successLabel
ReactNode

Content shown in success state. Defaults to "Done".

errorLabel
ReactNode

Content shown in error state. Defaults to "Failed".

renderState
(state: ActionButtonState) => ReactNode

Full render control. Overrides individual label/icon props. Receives the current state.

announcements
Partial<Record<ActionButtonState, string>>

Per-state strings announced via aria-live region for screen readers.

variant
"solid" | "outline" | "ghost" | "link"

Visual style inherited from Button. Defaults to "solid".

size
"sm" | "md" | "lg"

Button size inherited from Button. Defaults to "md".

disabled
boolean

Disables the button. Also disabled automatically while pending.

className
string

Additional CSS classes merged via cn().

Installation

npx shadcn@latest addhttps://ui.nexvyn.dev/r/action-button.json

How to use

import { ActionButton } from "@/components/ui/action-button"

export function Demo() {
  return (
    <ActionButton
      onAction={async (signal) => {
        const res = await fetch("/api/save", { signal, method: "POST" })
        if (!res.ok) throw new Error("Save failed")
      }}
      idleLabel="Save"
      pendingLabel="Saving…"
      successLabel="Saved"
      errorLabel="Retry"
      minPendingMs={600}
      resetDelayMs={2000}
      announcements={{
        pending: "Saving…",
        success: "Saved successfully",
        error: "Save failed, click to retry",
      }}
    />
  )
}

Source Code

Click the code icon in the top-right corner to view the source code.

Contact

Questions or feedback? Reach out anytime.

License & Usage

  • Free for personal and commercial use
  • No attribution required
  • Cannot be resold as a standalone product
  • Anatomy/blueprint diagrams are licensed separately (CC BY-NC 4.0) and are not for commercial use
// No code loaded.

Click to trigger async state machine

npx shadcn@latest addhttps://ui.nexvyn.dev/r/action-button.json