A shadcn/ui button component that provides clear visual feedback with full accessibility support for any asynchronous action.
- Multiple states:
idle,loading/progress,success,error - Async-friendly: works seamlessly with Promise-based handlers
- Powered by XState: predictable and robust state transitions
- Accessible by default: ARIA messages and screen reader support
- Fully customizable: shipped as source code via
shadcn/uiCLI
Add the StatefulButton to your project using the shadcn/ui CLI:
npx shadcn@latest add https://stateful-button.vercel.app/r/stateful-button.jsonThis will add stateful-button.tsx to your components/ui directory and stateful-button-machine.ts to your lib directory.
A stateful button represents the lifecycle of an action, typically an asynchronous one. It provides immediate visual feedback to improve responsiveness and prevent confusion.
This component manages the following states:
idle: the default, interactive state.loading/progress: indicates that an operation is in progress.success: confirms that the operation completed successfully.error: shows that something went wrong.
This component is perfect for any time-consuming operation where you need to keep users informed that a task is running and awaiting completion. Common examples include:
- Slow API Requests: submitting forms, completing e-commerce checkouts, or any backend communication that might have a noticeable delay.
- Data Processing: processing or saving complex data.
- File Operations: uploading large files.
- And any other asynchronous task where clear user feedback is critical.
The Stateful Button supports two modes: spinner and progress.
This is the default mode. The button shows a spinner while the onClick handler is executing.
import { StatefulButton } from '@/components/ui/stateful-button';
export default function SaveExample() {
return (
<StatefulButton
onClick={async () => {
// Simulate an API call
await new Promise((resolve) => setTimeout(resolve, 2000));
}}
>
Save
</StatefulButton>
);
}In this mode, the button displays a progress bar. The progress prop must be a controlled value between 0 and 100.
import React from 'react';
import { StatefulButton } from '@/components/ui/stateful-button';
export default function UploadExample() {
const [progress, setProgress] = React.useState(0);
const handleUpload = async () => {
// Simulate upload progress
for (let i = 0; i <= 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 250));
setProgress(i * 10);
}
};
return (
<StatefulButton
buttonType="progress"
progress={progress}
onClick={handleUpload}
onComplete={() => console.log('Upload complete')}
>
Upload
</StatefulButton>
);
}The StatefulButton component extends standard HTML button attributes and shadcn/ui's button variants, in addition to its own specific props:
| Prop | Type | Default | Description |
|---|---|---|---|
onClick |
(event: React.MouseEvent<HTMLButtonElement>) => void | Promise<unknown> |
- | Click handler invoked when the button is pressed. Can return a Promise if the click handler is asynchronous. |
onComplete |
() => void |
- | Callback triggered when the action completes successfully. |
onError |
(error: Error) => void |
- | Callback triggered when onClick throws (or a rejection occurs). |
buttonType |
'spinner' | 'progress' |
'spinner' |
Specifies the button's behavior mode. 'spinner' shows a loading spinner. 'progress' displays a progress bar. |
progress |
number |
- | The current progress value (0-100). This is a controlled prop used to update the progress bar. Only applicable when buttonType is 'progress'. |
children |
React.ReactNode |
- | Content to render inside the button while in the idle state. |
ariaMessages |
AriaMessages |
Default English messages for all states. | Customizable ARIA messages for accessibility. |
variant |
string |
'default' |
The visual style of the button (e.g., default, destructive, outline). Inherited from shadcn/ui's Button component. |
size |
string |
'default' |
The size of the button (e.g., default, sm, lg). Inherited from shadcn/ui's Button component. |
| ...other native button props | React.ButtonHTMLAttributes<HTMLButtonElement> |
- | All standard HTML button attributes are supported. |
The Stateful Button is managed by an XState finite state machine, defined in lib/stateful-button-machine.ts. This machine ensures predictable state transitions, making the component's behavior robust and preventing inconsistent UI states.
The Stateful Button is accessible and screen-reader friendly, using ARIA attributes and providing descriptive messages for its various states. You can further customize these messages using the ariaMessages prop, allowing you to tailor the spoken feedback to your specific application's needs.
The Stateful Button is tested using Cypress Component Testing.
Run tests in headless mode:
npm run cypress:runOr open interactive mode:
npm run cypress:openThis repository provides documentation, a live showcase, and a custom shadcn registry for the Stateful Button component.
To run this project locally, follow these steps:
-
Clone the repository:
git clone https://github.com/nanyx95/Stateful-Button-React.git
-
Navigate to the project directory:
cd Stateful-Button-React -
Install the dependencies:
npm install
-
Run the development server:
npm run dev
Licensed under the MIT license.
