Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Buttons

Pro

Download Button for React

A button that fills with a download's progress, from a download icon to a spinner to a check. For React with Base UI, for exports and large files where people want to see how far along it is.

Framework
UI library

You drive it with state and progress. At rest it shows a download icon; while downloading, a spinner and a fill behind the label that grows with progress, from 0 to 100; when complete, the fill tops up and fades as a check pops in. Pass resetDelay and it calls onStateChange("idle") after that many milliseconds, to bring it back to rest.

Use it where a file takes long enough to wait for: a report export, a video, a backup. For an instant download, a plain link is enough; for a wait you can't measure, use the Loading Button. It wraps the Button, so variant and size work as usual, and the fill takes the label's color, so it reads on every variant. The fill is one CSS variable that scales it along the line, and it and the rows move with CSS transitions, with no animation library; with reduced motion the rows only fade and the fill steps without sliding.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Usage

import { DownloadButton } from "@/components/ui/download-button";
<DownloadButton
  onClick={start}
  onStateChange={setState}
  progress={progress}
  resetDelay={2000}
  state={state}
/>

Demos

Labels

idleLabel, downloadingLabel and completeLabel set the words for each state: here Export PDF, Exporting… and Report ready. The button is as wide as the longest of the three, so it doesn't change size between them.

Preview width

States

The three states side by side, with the download at 45%.

Preview width

Accessibility

The icons, the fill and the other rows are hidden from assistive tech; the label at rest keeps naming the button. A polite status inside the button says downloadingLabel with the progress at each quarter, 0%, 25%, 50% and 75%, not every percent, and completeLabel when it ends. While downloading, the button is marked aria-disabled and ignores presses, but keeps its focus and its full color.

Enter
Start the download, unless one is running.
Space
Start the download, unless one is running.

FAQ

Does the button download the file?

No. It shows a download you run: set state to downloading in your press handler, update progress as bytes arrive, and set complete at the end. While it's downloading, your press handler isn't called.

Where does the progress come from?

With fetch, read the body with response.body.getReader(), add up the bytes and divide by the Content-Length header, times 100. Values below 0 or above 100 are clamped.

Does it go back to Download by itself?

Only with resetDelay: that many milliseconds after complete, it calls onStateChange("idle"); pass your state's setter there. With the default, 0, it stays on complete.

What do I show when the download fails?

Set state back to idle and say what happened next to it, in a toast or a message. The button only shows a download that's going or done.

How do I change Download to Export PDF?

Use idleLabel, downloadingLabel and completeLabel. The button takes no children, and it's as wide as the longest of the three labels, so it doesn't change size between them. The Labels demo shows all three.

What does a screen reader hear while it downloads?

The downloading label and the progress in steps of a quarter, such as Downloading… 50%, then the complete label. Reading every percent would talk over everything else.

What happens with reduced motion?

The rows swap with a fade and the fill jumps to each new value instead of sliding. The progress still shows.

API Reference

DownloadButton

data-slot="download-button-state"

PropType
state?DownloadButtonState

Where the download is. You run it from your own fetch and set this: the button shows it, and stops presses while downloading.

Default
"idle"
onStateChange?(next: DownloadButtonState) → void

Called with "idle" when the check has stayed resetDelay milliseconds.

Type
(next: DownloadButtonState) → void
Parameters

next – DownloadButtonState

Returns
void
completeLabel?ReactNode

The label once the download is done.

Default
"Downloaded"
downloadingLabel?ReactNode

The label while downloading, beside the spinner.

Default
"Downloading…"
focusableWhenDisabled?boolean

Whether the button should be focusable when disabled.

Default
false
idleLabel?ReactNode

The label at rest; it names the button in every state.

Default
"Download"
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
progress?number

How far the download is, from 0 to 100, while state is "downloading". The fill behind the label follows it; values outside the range are clamped.

Default
0
resetDelay?number

How long the check stays before onStateChange("idle"), in milliseconds; 0 keeps it until you change state.

Default
0
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

Mirrors state.

CSS variableType
--download-button-progressstring

How far the fill reaches, from 0 to 1.

All 35 buttons