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/ui
CLI
Add the StatefulButton
to your project using the shadcn/ui CLI:
npx shadcn@latest add https://stateful-button.vercel.app/r/stateful-button.json
This 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 | 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' |
Specifies the button's behavior mode. 'spinner' (default) 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 |
Customizable ARIA messages for accessibility. Defaults are provided if not supplied. |
variant |
string |
The visual style of the button (e.g., default , destructive , outline ). Inherited from shadcn/ui's Button component. |
size |
string |
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:run
Or open interactive mode:
npm run cypress:open
This 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.