shadcn.io is an independent project built on shadcn/ui
Two ways to wait. In the first group the pressed button disables and reads Copying..., Downloading..., or Sharing..., and its neighbors stay usable. In the second, pressing Action 1, Action 2, or Action 3 disables all three. A two-second timer turns them back on.
Change one label when the other actions are still valid. Lock the group when one action should finish first. A spinner in the label is a Spinner you swap in. In React, Base UI does not know when the request ends.
Installation
Anatomy

Usage
<div className="flex flex-col gap-4">
<ButtonGroup aria-label="Actions">
{ACTIONS.map((item) => (
<Button
disabled={loading === item.value}
key={item.value}
onClick={handleAction}
size="sm"
value={item.value}
variant="outline"
>
<item.icon aria-hidden="true" data-icon="inline-start" />
{loading === item.value ? item.pending : item.label}
</Button>
))}
</ButtonGroup>
<ButtonGroup aria-label="Grouped actions">
{GROUP_ACTIONS.map((item) => (
<Button
disabled={loading !== null}
key={item.value}
onClick={handleAction}
size="sm"
value={item.value}
variant="outline"
>
{item.label}
</Button>
))}
</ButtonGroup>
</div>FAQ
How is the running action tracked?
One value holds the running action's name, or nothing. Each button carries its name as its value; pressing it stores the name and starts a two-second timer that clears it.
How does the first group behave?
Only the pressed button is disabled and shows its pending label, such as Copying.... The other two stay enabled.
How does the second group behave?
Every button is disabled while any action runs, including one started from the first group. Their labels don't change.
What happens to the timer if the demo goes away?
It's cleared when the demo unmounts, and a new press clears the previous timer before starting its own.
How do I show a spinner instead?
Pass isLoading to the Button. It disables the button and draws a Spinner over the label, keeping the width. The demo changes the label instead.
API Reference
Button Group
Sets aria-label="Actions" and aria-label="Grouped actions". The rest is on the Button Group page.
Button
Sets disabled={loading === item.value}, onClick={handleAction}, size="sm", value={item.value}, variant="outline" and disabled={loading !== null}. The rest is on the Button page.





