Status Button

A button that shows loading and success feedback for async actions. Return a promise from onClick for the fully self-managed flow, or pass a status you control from your own form state — either way the content swaps in place with a spring animation, keeps its width fixed across states, and respects prefers-reduced-motion.

Last updated October 20, 2018

CreditsModifiedPublished

Copied from Chánh Đại's Status Button.

  • Replaced the shadcn/Radix Button with this repo's Base UI Button
  • Replaced radix-ui/internal's useControllableState with this repo's standalone useControllableState hook (shipped alongside the component)
  • Replaced the upstream's custom ten-spoke spinner — which required adding a spinner-opacity keyframe to global CSS — with this repo's existing Spinner, so installation needs no CSS edits
  • Omitted this repo's own Button `loading` prop from the public props to avoid two competing busy indicators

Overview

Installation

$ npx shadcn@latest add @persianlabsui/status-button

Usage

import { StatusButton } from "@/components/ui/status-button"
<StatusButton onClick={save} successLabel="Saved">
  Save
</StatusButton>

Return a promise from onClick. The button shows a spinner while the promise is pending, then the success label, then returns to idle. If the promise rejects, the button returns to idle right away.

While busy it is not disabled — focus is kept, aria-busy and aria-disabled announce the state to screen readers, and presses are ignored by the click handler.

Controlled

When a form owns the submission, pass status and onStatusChange instead of onClick. The button still returns to idle on its own, and onStatusChange receives "idle" when that happens.

import {
  StatusButton,
  type ButtonStatus,
} from "@/components/ui/status-button"
 
const [status, setStatus] = useState<ButtonStatus>("idle")
<StatusButton
  type="submit"
  status={status}
  onStatusChange={setStatus}
  successLabel="Sent"
>
  Send
</StatusButton>

Examples

Form Submission

A controlled StatusButton next to an Input: invalid input flips back to idle with an inline error, valid input shows the success label.

RTL

Persian labels swap correctly in RTL layouts. The stacked-grid layout and vertical swap motion are direction-agnostic, so no RTL-specific CSS is needed.

API Reference

StatusButton

See the Base UI Button documentation for the underlying props.