The uploader you’d build if you had the time

Drop Upload is a modern React file uploader with every state designed and animated, and every odd case already handled, so you don’t have to.

One payment, yours for good.

A 27 second film. It opens on the line: an upload box looks simple, until you try to build a good one. Three files are dragged onto Drop Upload and upload side by side, each with its own progress bar. One fails and is retried with a click. A file called holiday.png is turned away because its first bytes show it is a Windows program. The uploaded files go through one adapter that can point at Amazon S3, S3 multipart, Supabase, Cloudinary or your own API. Finally the avatar variant frames a photo inside a circle and uploads the crop.

Now try it yourself

This is the real component, running on this page. Throw some files at it and see what it lets through.

Try dropping files
image files or MP4. Max: 2 MB. Up to 5 files

Live demo. Your files never leave the browser.

Validation, built in

Say which types, sizes and how many files you take, and every file is checked before it uploads. It even reads a file’s first bytes, so an .exe renamed to .png gets turned away at the door.

Only the types you take

MIME types, and wildcards like image/* too. The hint under the box is written from the same list, so people know before they drop.

A cap on the list

Stop at five, or whatever you pick. People are told why the sixth one didn’t make it in.

Too big, too small

A floor and a ceiling in KB, shown to people in MB so the limit reads like a sentence.

It reads the first bytes

A name and a type are just labels. Drop Upload knows what 11 formats look like on the inside, so an .exe renamed to .png is caught at the door.

Folders turned away

Refused by name with a clear reason, instead of sitting in the list as an empty file that fails later.

Reasons people understand

Every refusal comes in English, Spanish, French or German. Or in your own words, one string at a time.

These checks save people from waiting on an upload that was never going to work. Your server should still check again.

Uploads anywhere

S3, S3 multipart for the big stuff, Supabase, Cloudinary or an endpoint of your own. Swap one prop and the same box sends files somewhere else.

Progress on every file

Each file gets its own bar and percentage, and a check when it lands.

Cancel that really cancels

One AbortController per upload. Stop one file and the rest carry on.

Retry in place

A failed file keeps its spot in the list, with a Retry button right under its name.

Now or later

Uploads start on drop, or set autoUpload to false and send them when the form is submitted.

Every adapter has a setup guide, server side included, in the docs.

Faces welcome

variant="avatar" turns the same component into a profile picture field. People drag and zoom their photo inside the circle, and the crop is what gets uploaded.

A short loop of the avatar variant. Photos of faces float around an empty circle. A pointer drops a selfie on it, zooms in with the slider, drags the face to the middle and uploads the crop, then holds the remove button to take it out. It drops a second photo and frames it, drops a third straight on top to replace it, and removes that one too.
Cropped before upload
Your adapter receives the framed square, 512 px unless you pick another size.
Arrow keys work too
Framing a photo doesn’t need a mouse. Hold Shift for bigger steps.
Replace, don’t pile up
A new photo swaps the old one. A file that fails the checks never evicts a good one.
Same engine inside
Validation, adapters, languages and callbacks behave exactly as they do in the box.

The little things

Nobody puts these on a roadmap. They’re in the box anyway.

Your AI can skip the first draft

Any AI can write you an upload box in a minute. Then somebody has to find its bugs. Drop Upload has already been through that, so your agent spends the session wiring it into your app instead of rediscovering them.

You ask your agent

Use AvatarUpload from src/components/DropUpload on the account settings page. Upload through createS3PresignedAdapter with a new /api/s3/presign route, and save file.url as the user's avatar in onUploadComplete. Don't change any file inside DropUpload.

Its job, start to finish
  • Reads index.ts and the typed props
  • Adds AvatarUpload to the settings page
  • Writes the presign route on your server
Wired in. Nothing inside DropUpload touched.
No longer your agent’s problemThe headliners
  • A folder dropped on the box
  • An .exe renamed to .png
  • The same file picked again after discarding it
  • Retry pressed while the first attempt is still running
  • A server that answers after the user cancelled
  • File names with # or ? inside a storage URL
  • Two uploaders on one page and a single paste
  • A video the browser can’t decode

Found, fixed and tested before you got here. The source explains every one in its comments, so your agent can read why, too.

And the long tail nobody plans for

  • minFileSize set higher than maxFileSize
  • An image pasted into a text box next to the uploader
  • Progress that arrives after a cancel
  • A success that arrives after a cancel
  • An upload still running when the component unmounts
  • A file discarded halfway up
  • A folder’s made-up file size
  • A video frame that takes more than 3 seconds
  • Space scrolling the page instead of opening the picker
  • A drop the file cap turns away entirely
  • A file name long enough to break the row
  • A transparent PNG lost against the background
  • maxFiles={1} with a picker that allows many
  • A bad photo trying to replace a good avatar
  • Several photos dropped on one avatar
  • Hold to discard with only a keyboard
  • Collecting ETags for S3 multipart
  • The wrong theme flashing on first render
  • Pasted text caught by a file listener
  • A type with no known signature flagged by mistake
  • Reduced motion switched on in the OS
  • Cancelling one upload and losing the others
  • A tall photo squashed into a square thumbnail

More prompts that work, in the docs

The code is yours

You get the whole folder, in TypeScript and in plain JavaScript, readable and commented. React and motion are the only things it needs. Change anything you like. It’s your code now.

What you download
  • DropUpload.zip
  • REQUIREMENTS.mdwhat to install
  • ts/DropUploadTypeScript source
  • js/DropUploadthe same, plain JavaScript
Inside the folder
  • Drop area and file list
  • Avatar with crop and zoom
  • Five upload adapters
  • Four languages
  • Typed props and callbacks
  • CSS Modules
  • Your licence
Up and running
1
Copy the DropUpload folder into your project.
2
Install the one library it uses.
$ pnpm add motion
3
Render it. That’s a working uploader.
page.tsx
1import { DropUpload } from "./components/DropUpload"2 3export default function Page() {4  return <DropUpload />5}
Styles you can change
Every colour is a --du-* variable you can override. The rest is plain CSS Modules, small commented files you can edit. Nothing to fight with your Tailwind setup.
Goes where React goes
Next.js, Vite, Remix or Astro. It’s a folder of components, so there’s nothing to configure.
Updates for life
Every future version at no extra cost, plus a year of email support from the person who wrote it.

Pay once. Keep it.

No subscription and no account. If it turns out wrong for your project, email within 30 days and you get your money back.

Personal

For you, on projects you or your employer own

$29once

Launch price. $39 after .

  • One developer
  • Unlimited projects of your own, commercial included
  • Lifetime updates
  • 12 months of email support
Get Personal

Team

For teams, agencies and client work

$89once

Launch price. $119 after .

  • Up to 10 developers
  • Unlimited projects, client work included
  • Lifetime updates
  • 12 months of email support
Get Team

Not sure which one? The FAQ answers it in two questions.

Go build the rest of your app

The upload box is taken care of.