Stateful Button
A state-machine button running idle → loading → success / error, switching automatically from an async onClick, with the content crossfading and the width morphing along with it.
npx shadcn@latest add https://webberui.com/r/stateful-button.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<StatefulButton />
Installation
npx shadcn@latest add https://webberui.com/r/stateful-button.jsonOr, once registries are configured in components.json, install it as @webberui/stateful-button.
Usage
import { StatefulButton } from "@/components/ui/stateful-button";
async function save() {
await fetch("/api/save", { method: "POST" });
}
<StatefulButton onClick={save} successChildren="Saved" errorChildren="Failed">
Save
</StatefulButton>;As long as onClick returns a Promise, clicking moves the button into loading automatically; once the Promise resolves it shows success, once it rejects it shows error, and after 1.5 seconds it returns to idle on its own.
Controlled mode
Passing state switches it to controlled mode, which turns off the built-in automatic transitions and reset — the state is entirely driven from outside:
const [state, setState] = React.useState<"idle" | "loading" | "success" | "error">("idle");
<StatefulButton state={state} onClick={() => setState("loading")}>
Submit
</StatefulButton>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
state | "idle" | "loading" | "success" | "error" | — | Controlled state; when provided, it is driven from outside and the built-in automatic transitions and reset are turned off |
onClick | (e) => void | Promise<unknown> | — | Click callback; when uncontrolled, returning a Promise runs the state machine automatically |
children | React.ReactNode | — | Content shown in idle / loading |
successChildren | React.ReactNode | children | Content for the success state |
errorChildren | React.ReactNode | children | Content for the error state |
disabled | boolean | false | Disables the button; when uncontrolled it is disabled automatically while loading |
All other native <button> attributes are forwarded (except onDrag / onDragStart / onDragEnd / onAnimationStart / onClick).
State machine
idle: the default state, clickable, with slight scale feedback on pressloading: the spinner keeps rotating, and when uncontrolled the button is locked to prevent double submissionsuccess: a green background, with the checkmark drawing itself in viapathLengtherror: a red background, with the cross drawn in stroke by stroke and the whole button shaking from side to side once- Content switches crossfade with
AnimatePresence(popLayout), and the button width morphs along withlayout
Accessibility
aria-busymarks the busy state while loading- A built-in
aria-live="polite"status region announces "處理中/成功/錯誤" as the state changes - When the user has "reduce motion" enabled at the system level, the rotation, shake, and offsets are disabled and the checkmark / cross appear instantly, while the content switch is kept so the meaning is preserved
Draggable Snap Button
A button you can drag that springs back and snaps: wherever you drag it, on release a spring snaps it to the nearest snap point — with multiple snap points, arrow-key switching, and a pure spring-back mode.
Shimmer Button
A shimmer button with light flowing around its edge, an inner highlight lining, and dimensional hover feedback.