Tutorial · September 29, 2026 · 5 min read
How to build a drag and drop file upload with progress in React
To build a drag and drop file upload with progress in React, install ReactFrame's free File Upload with npx shadcn@latest add @reactframe/file-upload, set accept, maxSize and maxFiles, and pass an upload(file, onProgress) function. Use XMLHttpRequest inside it, because fetch can't report upload progress, and call onProgress with a percentage.
An upload field needs more than an <input type="file">: a drop zone, clear errors for the wrong type or size, a preview, and progress for big files. This tutorial uses ReactFrame's free File Upload and sends the files to a Next.js route handler.
1. Install
npx shadcn@latest add @reactframe/file-upload
2. Set the rules
import { FileUpload } from "@/components/file-upload";
<FileUpload
accept="image/*,.pdf"
maxSize={5 * 1024 * 1024}
maxFiles={4}
onFilesChange={(files) => console.log(files)}
/>
Files that break a rule stay in the list with a reason ("File type not allowed", "Larger than 5.0 MB", "Limit of 4 files reached") instead of silently disappearing. Images get a thumbnail preview. onFilesChange receives the valid files whenever the list changes, which is all you need if the files are sent later with a form.
3. Upload with real progress
Pass an upload function and each file uploads as soon as it's added, with its own progress bar. fetch can't report upload progress, so use XMLHttpRequest:
"use client";
import { FileUpload } from "@/components/file-upload";
function uploadFile(file: File, onProgress: (percent: number) => void) {
return new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/upload");
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress((e.loaded / e.total) * 100);
};
xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error("Upload failed")));
xhr.onerror = () => reject(new Error("Network error"));
const body = new FormData();
body.append("file", file);
xhr.send(body);
});
}
export function Attachments() {
return <FileUpload accept="image/*,.pdf" maxSize={5 * 1024 * 1024} upload={uploadFile} />;
}
If the promise rejects, the file shows your error message and a retry button; the remove button works at any point.
4. Receive the file in Next.js
// app/api/upload/route.ts
export async function POST(request: Request) {
const form = await request.formData();
const file = form.get("file");
if (!(file instanceof File)) return new Response("No file", { status: 400 });
if (file.size > 5 * 1024 * 1024) return new Response("Too large", { status: 413 });
const bytes = Buffer.from(await file.arrayBuffer());
// Save `bytes` to your storage (S3, R2, Vercel Blob...) under a name you generate.
return Response.json({ ok: true });
}
Security checklist
- Check type and size on the server again.
acceptandmaxSizeare for the user's convenience; anyone can bypass them. - Never trust the original filename. Generate your own key, and don't serve uploads from the same origin as your app without a strict content type.
- Large files: serverless functions have request size limits. For big uploads, have the server hand out a pre-signed URL and upload straight to storage from
uploadFile.
Other props
multiple={false} for a single file (an avatar), size ("sm", "md", "lg"), title, description and label for the text, theme ("dark" or "light"), width and disabled.

Ahmet Loca · Product designer
Hi, I'm Ahmet. I have over four years of experience creating intuitive, user-focused designs for digital products, and ReactFrame is where that work becomes components you can use in your own.