Easy Shadcn
Components

Async Button

AsyncButton extends shadcn's Button: when onClick returns a Promise, the component automatically manages the loading state — the button is disabled and a spinner is shown until the promise settles. You can also drive the loading state manually via the loading prop, and decorate the button with startIcon / endIcon slots that the spinner slides into while loading.

Click the button — loading state activates automatically while the promise is pending.

Drive loading externally via the loading prop.

Use startIcon / endIcon. While loading, the icon slot is swapped for the spinner — no overlay needed. When both are present, startIcon takes the spinner.

Icon buttons with size="icon".

Anti-flash: a 50ms task still shows the spinner for at least 200ms (the default minDuration), so the indicator never just flickers past.

Errors are caught and logged — the button recovers gracefully.

Installation

With the @easy-shadcn namespace configured:

pnpm dlx shadcn@latest add @easy-shadcn/async-button

Or install via the full URL (zero configuration):

pnpm dlx shadcn@latest add https://easy-shadcn.vercel.app/r/async-button.json

Props

PropTypeDefaultDescription
onClick(e: MouseEvent<HTMLButtonElement>) => void | Promise<void>-Click handler. When it returns a Promise, the button enters the loading state until it settles.
loadingbooleanfalseControlled loading state, OR-ed with the internal async loading state.
startIconReactNode-Icon rendered before children. Replaced by the spinner while loading.
endIconReactNode-Icon rendered after children. Replaced by the spinner while loading when startIcon is absent.
disabledbooleanfalseDisables the button. The button is also disabled automatically while loading.

All other props (variant, size, className, etc.) are inherited from shadcn's Button and forwarded as-is.

Loading indicator placement

The spinner is rendered in the most context-appropriate place, in this priority order:

  1. size="icon"children (the lone icon) is replaced by the spinner.
  2. startIcon presentstartIcon is replaced by the spinner; endIcon (if any) stays put.
  3. Only endIcon presentendIcon is replaced by the spinner.
  4. No icons — a blurred overlay covers the button and the spinner is centered on top.

This means the button never grows or reflows when loading kicks in.

Notes

  • Exceptions thrown from onClick are caught and logged via console.error so that unhandled promise rejections don't break the page, while still surfacing the error for observability.
  • The fallback overlay uses backdrop-blur so it works seamlessly with every button variant when no icon slot is available.
  • While loading, the button is disabled (no clicks, no hover) but keeps its full opacity — the disabled:opacity-50 style only applies when the button is purely disabled, not when it's loading.
  • The button sets aria-busy={loading} so assistive technologies can announce the loading state.

On this page