Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Buttons

Free

Loading Button for React

A button that shows a spinner beside its label while an action runs, and stops presses until it's done. For React with Base UI, on save and submit buttons that wait for a server.

Framework
UI library

Set isLoading and the label rolls up and out while a spinner and loadingText, such as Saving…, roll in; set it back and the label returns. Without loadingText or spinnerPosition, it's the Button's own loading state: a spinner centered over the hidden label. While it loads, presses do nothing and the button keeps its width, so nothing around it jumps.

Use it for any action that waits on a request: saving a form, sending a message, syncing data. When the action should also say it's done, use the Success Button. It wraps the Button, so variant and size work as usual. The two rows share one place and swap with CSS transitions, with no animation library; with reduced motion they only fade.

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.

Usage

import { LoadingButton } from "@/components/ui/loading-button";
<LoadingButton isLoading={saving} loadingText="Saving…" onClick={save}>
  Save changes
</LoadingButton>

Demos

Spinner Position

spinnerPosition puts the spinner at the start of the label or its end; the end reads well for an action that moves forward, such as Continue. Without loadingText, the children stay beside the spinner. Both sides follow the reading direction, so they flip in a right-to-left page. The last button sets neither, and gets the Button's centered spinner.

Preview width

Form

As a submit button, it shows the request is on its way and ignores more presses until it ends. That includes Enter in the field: the press it makes is stopped too, so the form isn't sent twice.

Preview width

Accessibility

The spinner and the loading row are hidden from assistive tech; the label at rest keeps naming the button. When loading starts, a polite status inside the button says loadingText, or Loading without it. The button is marked aria-disabled and stays focusable while it loads, so a keyboard user who pressed it isn't sent back to the top of the page. With the centered spinner, it's the Button's own loading state, which is disabled and aria-busy.

Enter
Activate the button, unless it's loading.
Space
Activate the button, unless it's loading.

FAQ

How do I show Saving… while it loads?

Pass loadingText="Saving…". It sits beside the spinner and the status says it too.

Can the spinner go after the label?

Yes, with spinnerPosition="end"; start puts it before. Leave out both spinnerPosition and loadingText for a spinner centered over the label. The Spinner Position demo shows all three.

Do I need to disable it myself while it loads?

No. While isLoading is on, the button is aria-disabled and ignores presses, and it keeps its focus. Your click handler isn't called while it loads.

Why doesn't the button change width?

The label and the loading row share one grid cell, so the button is as wide as the wider one in both states. A short label may look a little padded at rest; that's the space the spinner needs.

How is it different from Button's isLoading?

It's the same prop. With nothing else, Loading Button is the Button: a centered spinner over the hidden label. loadingText and spinnerPosition add what the Button lacks: a label beside the spinner, and a roll between the two rows.

What happens with reduced motion?

The rows swap with a fade and don't slide. The spinner keeps turning, since it's the sign that something is happening.

API Reference

LoadingButton

data-slot="loading-button-label"

PropType
children?ReactNode

The label at rest; it names the button in both states.

focusableWhenDisabled?boolean

Whether the button should be focusable when disabled.

Default
false
loadingText?ReactNode

The label while loading, such as "Saving…", beside the spinner. Leave it and spinnerPosition out for the Button's own centered spinner.

nativeButton?boolean

Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).

Default
true
spinnerPosition?"start" | "end"

Which side of the label the spinner sits on while loading: start is the left in a left-to-right page, the right in a right-to-left one. Leave it and loadingText out for the Button's own centered spinner.

Type
"start" | "end"
render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Merged with the part's classes.

AttributeType
data-state?string

"loading" when loading is true, otherwise "idle".

All 35 buttons