Skip to content

shadcn.io is an independent project built on shadcn/ui

Free

Loading States Button Group for React

Copy, Download, and Share switch to a pending label for two seconds, and a second group locks every button.

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.

Preview

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.
Use email insteadNo credit card

Anatomy

Button Group anatomy: root, group, button, separator

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.

How do I use a real request?

Set the value before the request starts and clear it when the request settles, success or error, instead of using the timer.

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.