React file upload component with two progress bars — one for reading the file from the user's hard disk or SSD into the browser, and a separate one for uploading it to your server. Drag and drop, TypeScript, zero dependencies.
When someone picks a file, the browser does not have it yet. The file is still sitting on their hard drive or SSD, and it has to be read from that disk into the browser's memory before your JavaScript can touch it. For a big video or a large PDF that read takes real time, and until now users stared at a frozen screen while it happened.
This library shows a progress bar for that read — from the hard disk / SSD into the browser. No server, no network, no upload. Just "your file is being loaded, here's how far along it is".
The GIF above is slowed down on purpose so you can see the bar move. On a fast local SSD, reading a 150MB file into the browser takes only about 90 milliseconds — measured, not guessed. The bar earns its keep where that read is genuinely slow: network drives and SMB shares, cloud-synced folders like iCloud or Google Drive, external USB disks, SD cards, and phones. On a quick SSD it simply flashes past, which is the correct behaviour.
Sending that file to your server afterwards is a completely separate step that you trigger yourself — with its own, second progress bar.
- Zero dependencies, no CSS import, TypeScript types, SSR safe
- Works with no server at all —
urlis optional - Every part swappable: your own card, your own bar, or no UI at all
npm install --save react-file-progressReact 16.8 or newer is required (it is a peer dependency).
add file ──▶ loading ──▶ ready ──▶ uploading ──▶ uploaded
automatic file is only when you call
in browser upload()
A file always loads into the browser, with a progress bar. It then sits at
ready — name shown, bytes available on item.data — until you upload it. Set
uploadFileOnLoad if you'd rather that happen automatically.
In other words: stage one is disk → browser (reading the file off the hard drive), stage two is browser → server (the actual upload). Most libraries only show you the second one.
Just load files into the browser. No server involved:
import { FileUpload } from 'react-file-progress'
<FileUpload
accept='image/*'
maxSize={10 * 1024 * 1024}
onLoadComplete={(item) => console.log(item.file.name, item.data)}
/>Add a url and each ready file gets an Upload button:
<FileUpload url='/api/upload' onUploadComplete={(item) => console.log(item.response)} />Or skip the wait and upload as soon as each file lands:
<FileUpload url='/api/upload' uploadFileOnLoad />| Prop | What it does | Default |
|---|---|---|
url |
Where upload() sends files. Omit it to only load into the browser |
— |
uploadFileOnLoad |
Upload each file as soon as it finishes loading | false |
accept |
Allowed file types, same as the input attribute: '.pdf,image/*' |
all |
maxSize |
Largest allowed file, in bytes | ∞ |
onLoadComplete |
File is in the browser — item.data holds the bytes |
— |
onUploadComplete |
Server accepted it — item.response holds the reply |
— |
onError |
Loading, uploading or validation failed | — |
method headers fieldName |
Request shape — auth tokens go in headers |
POST · — · 'file' |
multiple disabled label hint |
Drop zone basics | true · false |
progressBar |
Props for every bar (see below) | — |
components classNames |
Swap parts out, or just restyle them | — |
children |
Render prop — your UI, this library's logic | — |
Rejected files appear in the same list as errors, so there's only one thing to render.
The bar — progressBar forwards to every bar:
<FileUpload url='/api/upload' progressBar={{ width: '100%', height: 10, color: '#8b5cf6', showLabel: true }} />value status width height color trackColor radius showLabel formatLabel indeterminate. Numbers become px. It's exported standalone too: <ProgressBar value={62} showLabel />.
Whole components — override one, keep the rest:
<FileUpload components={{ Container: MyCard, ProgressBar: MyBar }} />| Slot | Gets |
|---|---|
Container |
children, className, style |
DropZone |
isDragging, disabled, label, hint, browse() |
Item |
item, upload(), cancel(), retry(), remove(), canUpload |
ProgressBar |
value, status, plus everything above |
CSS — class names are stable and unhashed (rfu-root, rfu-item, rfu-progress…), bars carry data-status, and colors are custom properties:
.rfu-root {
--rfu-bar-color: #3b82f6; /* uploading */
--rfu-bar-color-loading: #a78bfa; /* loading into the browser */
--rfu-success: #22c55e;
--rfu-danger: #ef4444;
--rfu-surface: #fff;
--rfu-text: #111;
--rfu-radius: 12px;
}Same logic, no UI:
const { items, addFiles, upload, cancel, retry, remove, clear, isBusy } =
useFileUpload({ url: '/api/upload' })Each item is { id, file, data, status, loadProgress, uploadProgress, error, response },
where status is loading | ready | uploading | uploaded | error and data is
the loaded Blob. upload() sends every ready file; upload(id) sends one.
item.data is kept until you remove() the file, so previews and re-uploads
never re-read from disk. Several large files therefore stay in memory — call
remove() or clear() when you're done with them.
How do I show a progress bar while a file loads into the browser in React?
Render <FileUpload /> with no url. Every file added is read from disk with a
live progress bar and ends up at ready, with its bytes on item.data. Nothing
is sent anywhere.
How do I show upload progress without uploading immediately?
That is the default here. The file loads into the browser and waits. Call
upload() from your own button when you're ready, or set uploadFileOnLoad to
send it automatically.
Why doesn't my upload progress bar work with fetch?
fetch cannot report upload progress portably — streaming request bodies need
duplex: 'half', which is Chromium-only and requires HTTP/2. This library uses
XMLHttpRequest, whose upload.onprogress works everywhere.
How do I read a large file in the browser without freezing the UI?
Blob.stream() reads it in chunks so the main thread stays responsive, and each
chunk updates progress. This library does that for you and falls back to
FileReader on older browsers.
How do I limit file type and size?
accept='image/*,.pdf' and maxSize={10 * 1024 * 1024}. Rejected files appear
in the same list with an error, so there's only one thing to render.
Does it work without the built-in UI?
Yes — useFileUpload() gives you the same logic headless, or pass a children
render prop to keep the logic and write your own markup.
Loading uses Blob.stream() where available (Chrome 76+, Firefox 102+, Safari
14.1+) and falls back to FileReader, which reports progress just as
accurately. Uploads use XMLHttpRequest because fetch still can't report
upload progress portably — streaming request bodies need duplex: 'half',
which is Chromium-only and requires HTTP/2.
npm install # installs and builds
npm run verify # generated-file check, lint, typecheck, tests, build
cd example && npm install && npm run devThe example is the live demo,
and npm run deploy publishes it to GitHub Pages. Running it locally is worth
it: the dev server has a deliberately throttled upload endpoint built in, so
the bars move at a watchable speed instead of finishing instantly.
Styles live in src/styles.css — the single source of truth. A script turns it
into src/styles.ts (injected at runtime) and copies it to dist/index.css;
both run automatically on build and test. Edit the .css, never the generated
.ts. Tests drive a fake XMLHttpRequest, because jsdom won't fire
upload.onprogress without a real network stack.
MIT © codeAesthetic


