crucıble

Piston

A stateful submit button that runs its async lifecycle like a piston stroke: label and spinner slide along one axis while the width breathes, a check draws itself on success, and rejection plays a shake-and-retry beat. Pass an async onClick and it owns the state machine — aria-busy, live announcements, and real disabling included.

motionfree

Installation

CLI

npx shadcn@latest add @crucible/piston

Manual — install dependencies, then copy the source

npm install motion

Props

PropTypeDefaultDescription
onClick(event: React.MouseEvent<HTMLButtonElement>) => void | Promise<unknown>Async click handler — return a promise and Piston runs the whole lifecycle for you: pending while in flight, success/error on settle, back to idle after the dwell. A sync handler leaves the button idle.
statePistonStateControlled state. When set, the internal machine is off: Piston renders exactly this state and you own every transition (onClick still fires).
childrenReact.ReactNode"Submit"Idle label.
pendingContentReact.ReactNodea neutral spinnerPending content.
successContentReact.ReactNodea self-drawing checkSuccess content.
errorContentReact.ReactNode"Retry"Error / retry label — the button stays live so a click retries.
contractToCirclebooleanfalseContract the button toward a circle while pending.
successDwellnumber1.6Seconds the success state dwells before the label returns.
shakeIntensitynumber1Error shake amplitude multiplier — 0 disables the shake.
spring{ stiffness?: number; damping?: number }{ stiffness: 520, damping: 34 }Spring for the axial content slide and the width breath.
disabledWhilePendingbooleantrueReally disable the button (not just visually) while pending.
colors{ success?: string; error?: string }{ success: "#34d399", error: "#f87171" }The only saturated moments.
size"sm" | "md" | "lg""md"Button size.
Also accepts all props of Omit<HTMLMotionProps<"button">, "onClick" | "children" | "animate" | "transition">they pass through to the underlying element.

Honors prefers-reduced-motion with a designed static fallback, and passes className through.