shadcn.io is an independent project built on shadcn/ui
Buttons
ProDownload Button for Vue
Fills with a download's progress, from a download icon to a spinner to a check. For Vue with Ark UI, for exports and large files people wait on.
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
<script setup lang="ts">
import { DownloadButton } from "@/components/ui/download-button";
</script>
<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"
clickEffect?boolean
Apply a click effect to the button.
- Default
true
completeLabel?VNode
The label once the download is done.
- Default
"Downloaded"
downloadingLabel?VNode
The label while downloading, beside the spinner.
- Default
"Downloading…"
idleLabel?VNode
The label at rest; it names the button in every state.
- Default
"Download"
isLoading?boolean
Show a loading indicator.
- Default
false
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
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
@state-change(next: DownloadButtonState) → void
Called with "idle" when the check has stayed resetDelay milliseconds.
- Type
(next: DownloadButtonState) → void- Parameters
next– DownloadButtonState- Returns
void
data-state?string
Mirrors state.
--download-button-progressstring
How far the fill reaches, from 0 to 1.





