shadcn.io is not affiliated with official shadcn/ui
Drive it with state: idle shows the label, loading a spinner with Loading…, and success a check with Done. The check pops in and draws itself. After resetDelay, two seconds by default, it calls onStateChange("idle") so you can set it back.
Use it for actions people want confirmed in place: saving settings, adding to a cart, sending an invite. For a long list of results or an error, a toast or an inline message says more; for a wait with no confirmation, the Loading Button is enough. It wraps the Button, so variant and size work as usual. The rows share one place and swap with CSS transitions, and the check's stroke is a CSS keyframe animation the install adds to your stylesheet, with no animation library; with reduced motion the rows only fade and the check appears whole.
Installation
Usage
import { SuccessButton } from "@/components/ui/success-button";<SuccessButton
onClick={save}
onStateChange={setState}
state={state}
successLabel="Saved"
>
Save changes
</SuccessButton>Demos
Color
successColor colors the check, any CSS color; by default it takes the label's color. Here an outline button's check turns green once the item is in the cart.
Staying Done
resetDelay={0} keeps the check until you set state back to idle yourself. It suits an action that happens once, like sending an invite.
Accessibility
The spinner, the check and their rows are hidden from assistive tech; the label at rest keeps naming the button. A polite status inside the button says loadingLabel when it starts and successLabel when it's done. While loading, the button is marked aria-disabled and ignores presses, but keeps its focus.
- Enter
- Activate the button, unless it's loading.
- Space
- Activate the button, unless it's loading.
FAQ
How do I show the check?
Set state="loading" when your request starts and state="success" when it resolves. The button doesn't run the request; it shows where it is.
Does it go back to the label by itself?
It asks to: resetDelay milliseconds after success, 2000 by default, it calls onStateChange("idle"). Pass your state's setter there. With resetDelay={0}, the check stays until you change state.
Can I change Loading… and Done?
Yes, with loadingLabel and successLabel, such as Saving… and Saved. The button is as wide as the widest of the three rows, so it doesn't change size between them.
Can the success state use another color?
successColor colors the check, such as var(--color-emerald-600). The rest keeps the variant's colors, so the label stays readable on every variant. The Color demo uses it on an outline button.
Can it be pressed while it loads?
No. While state is loading it's aria-disabled, presses are ignored, and it stays focusable. In the success state it can be pressed again.
Your click handler isn't called while it loads.
API Reference
SuccessButton
data-slot="success-button-label"
state?SuccessButtonState
Where the action is: at rest, running (a spinner, and presses stop) or done (a check that draws itself). You set it from your request.
- Default
"idle"
onStateChange?(next: SuccessButtonState) → void
Called with "idle" when the check has stayed resetDelay milliseconds.
- Type
(next: SuccessButtonState) → void- Parameters
next– SuccessButtonState- Returns
void
children?ReactNode
The label at rest; it names the button in every state.
focusableWhenDisabled?boolean
Whether the button should be focusable when disabled.
- Default
false
loadingLabel?ReactNode
The label while state is "loading", beside the spinner.
- Default
"Loading…"
nativeButton?boolean
Whether the component renders a native <button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>).
- Default
true
resetDelay?number
How long the check stays before onStateChange("idle"), in
milliseconds; 0 keeps it until you change state.
- Default
2000
successColor?string
The check's color, any CSS color. By default the label's color.
successLabel?ReactNode
The label once state is "success", beside the check.
- Default
"Done"
render?ReactElement | ((props: object, state: object) => ReactElement)
Replace the host element or compose it with another component.
- Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string
Merged with the part's classes.
data-state?string
Mirrors state.
--success-button-colorstring
The check's color (the label's by default).
Related
Loading Button
Progress & resultsJust the waiting part
- Pro
Confetti Button
Motion & feedbackCelebrate a bigger win
- Pro
Shake Button
Motion & feedbackThe failure side of the same flow
Copy Button
Progress & resultsA check after copying text
- Pro
Download Button
Progress & resultsProgress, then a check
- Pro
Slide To Confirm Button
ControlsConfirm before the action runs