shadcn.io is not affiliated with official shadcn/ui
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.
Installation
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.
States
The three states side by side, with the download at 45%.
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.
API Reference
DownloadButton
data-slot="download-button-state"
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.
data-state?string
Mirrors state.
--download-button-progressstring
How far the fill reaches, from 0 to 1.
Related
Loading Button
Progress & resultsWhen progress can't be measured
- Pro
Success Button
Progress & resultsIdle, loading, done for any action
Countdown Button
Progress & resultsA fill that drains over time
Copy Button
Progress & resultsCopy text instead of saving a file
- Pro
Long Press Button
ControlsA fill that grows while held
- Pro
Border Trace Button
Glow & lightProgress traced around the edge