Button
Buttons in every variant and size, with a built-in loading, success and error flow that skips the spinner for fast requests.
pnpm dlx shadcn@latest add https://hextaui.com/r/button.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cnCopy and paste the following code into your project.
components/ui/button.tsx Update the import paths to match your project setup.
With feedback, return a promise from onClick and the button shows loading, then success or error, then resets on its own.
Variants
Six variants. destructive is a soft tint so a dangerous action reads clearly without shouting.
Sizes
Text sizes xs to lg, and square icon-* sizes. Small icon buttons get a larger invisible touch area on touch screens.
With icon
Mark an icon with data-icon="inline-start" or "inline-end" and the padding on that side tightens to balance it.
Disabled
focusableWhenDisabled keeps a disabled button in the tab order, so a tooltip or explanation can still be reached by keyboard.
Custom labels
loadingLabel, successLabel and errorLabel replace the text for each state. Each label flips in while the old one flips out.
Smooth width
The button eases to the width of each label instead of reserving space for the longest one, so nothing around it jumps.
Error details
Pass a function to errorLabel to show the rejection reason. While the pointer or keyboard focus stays on the button, the error stays on screen.
Forms
For submit buttons, call track() from useButtonFeedback in onSubmit and spread buttonProps on the button. Remove the @ to see the error.
Icon buttons
Icon sizes swap only the icon for each state and keep their square shape. The aria-label stays the accessible name.
Feedback on every variant
Filled variants turn green or red when done. ghost and link only change their text color.
Controlled loading
Set loading yourself when the work is tracked elsewhere. The button stays focusable and announces that it is busy.
Controlled status
Drive status directly, for example from a form library’s submit state.
As a link
Pass an anchor to render and set nativeButton={false} so the button keeps link semantics.
Right to left
Icons and state labels follow the reading direction.
| Key | Action |
|---|---|
| EnterSpace | Activates the button. Ignored while a feedback request is in flight. |
| Tab | Moves focus. A loading button stays focusable, and focusing an error keeps it on screen until you move away. |
- Every state change is announced through a polite live region: loading, then the success or error label.
- While loading, the button sets
aria-busyand stays focusable, so focus is never lost mid-request. - The spinner appears only after 150ms and then stays for at least 400ms, so fast requests never flash and slow ones never flicker.
- With reduced motion, state labels fade instead of flipping and the error shake is skipped.
Built on the Base UI button. It renders a <button> and accepts all of its attributes.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | "default" |
size | "xs" | "sm" | "default" | "lg" | "icon-xs" | "icon-sm" | "icon" | "icon-lg" | "default" |
feedbackTrack the promise returned from onClick and show its status. | boolean | false |
onClickReturn a promise to drive feedback. | (event) => unknown | – |
loadingControlled loading state. | boolean | – |
statusControlled status. Takes priority over loading. | "idle" | "loading" | "success" | "error" | – |
onStatusChange | (status: ButtonStatus) => void | – |
onErrorCalled with the rejection reason. | (error: unknown) => void | – |
resetAfterMilliseconds before returning to idle. | number | { success?: number; error?: number } | { success: 2000, error: 4000 } |
loadingLabelShown next to the spinner. Hidden on icon sizes. | ReactNode | – |
successLabel | ReactNode | "Done" |
errorLabel | ReactNode | (error: unknown) => ReactNode | "Failed" |
disabled | boolean | false |
focusableWhenDisabledAlways true while loading. | boolean | false |
nativeButtonSet to false when render is not a <button>. | boolean | true |
render | ReactElement | (props, state) => ReactElement | <button> |
| Attribute | Description |
|---|---|
data-slot="button" | Target buttons in CSS. |
data-status | idle, loading, success or error. Present once feedback, loading or status is used. |
data-disabled | Present when the button is disabled. |
aria-busy | Present while loading. |
Runs the same feedback flow from anywhere, such as a form’s onSubmit. Accepts resetAfter, onStatusChange and onError. See the useButtonFeedback guide for the full timing.
| Returns | Description |
|---|---|
track(action) | Pass a promise or a function returning one. Calls while a request is in flight are ignored. |
buttonProps | Spread on <Button> to show the status and pause the reset on hover and focus. |
status | The current ButtonStatus. |
error | The last rejection reason. |
reset() | Cancels the request and returns to idle. |
isPending() | Whether a request is in flight. |