Back to UI Components

MultiStateButton

Multi-state animated button with a slot-machine transition between states. Ships in two layers: MultiStateButton<K> is the generic primitive (any state union, no defaults); TransactionButton is a tx-flavored preset with the 7-state lifecycle and sensible defaults baked in. Most of the sections below use the preset; the last one drops down to the generic primitive.

State explorer

Click a pill to set the button state. The button only invokes onClick in the ready state.

clicks: 0

Lifecycle simulator

Programmatically cycles through a transaction flow at ~1 s per step.

Lifecycle simulator (fixed width)

Same simulator with className="w-64" applied — the slot-machine y-slide still plays, but the button width stays locked through the whole flow. Compare with the auto-width simulator above.

Fixed width

Pass a width via className (e.g. w-64) to lock the button width. Content still fades between states; no layout reflow.

Interactive non-ready states

Each state can opt-in to interactivity via disabled: false and an onClick. The "ready" state can also be soft-locked by setting disabled: true.

<TransactionButton
  state="finalized"
  states={{
    finalized: {
      content: <>↗ Finalized — click me</>,
      disabled: false,
      onClick: () => openExplorer(blockHash),
    },
  }}
/>

Custom content

Per-state content is fully replaceable — pass plain text, label + icon, or any JSX.

Generic MultiStateButton

Drop down to the underlying MultiStateButton<K> when your domain isn't a chain transaction. State keys are arbitrary; you own every per-state config (no defaults baked in).

state: idle
type GenericState = "idle" | "loading" | "success" | "error";

<MultiStateButton<GenericState>
  state={state}
  states={{
    idle:    { content: "Run job",  disabled: false, onClick: ... },
    loading: { content: <Spinner />, className: "..." },
    success: { content: "Done",     disabled: false, onClick: reset },
    error:   { content: "Retry",    disabled: false, onClick: reset },
  }}
/>

Usage

import { TransactionButton } from "@/components/ui/TransactionButton";

const [state, setState] = useState<TransactionButtonState>("ready");

<TransactionButton
  state={state}
  onClick={() => mutate()}
  states={{ ready: { content: "Submit transaction" } }}
/>