The uploader you’d build
if you had the time
Now try it yourself
This is the real component, running on this page. Throw some files at it and see what it lets through.
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.
- 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.
Paste to upload
Ctrl+V a screenshot and it’s attached. With two uploaders on a page, only one of them takes it.
Cancel mid-flight
Every upload has its own stop button. A late reply from the server can’t bring it back to life.
Real video thumbnails
Videos show a frame pulled from the clip itself, with a clean icon if the codec won’t cooperate.
Stacked mode
Files pile up as cards, so a long list never shoves the rest of your form down the page.
Hold to discard
Removing a file takes a deliberate press, so nobody loses one by accident. Set it to zero to skip it.
No mouse needed
Tab to it, Enter or Space to browse. Screen readers hear the same instructions everyone else reads.
Less motion on request
If someone asked their system for reduced motion, every animation in it listens.
Multi-language
English, Spanish, French and German built in, one
localeprop away. Reword any string, or bring a language of your own.Light and dark
You pass the theme in. It never guesses, so it renders the same on the server and never flashes.
Drive it from code
addFiles,removeFile,startUploadand friends on a ref, for when your app is in charge.Callbacks for everything
Drops, rejections with their reason, finished uploads with their URL, errors and cancels.
A puff of smoke
Files land with a small cloud right where they hit. Nobody asked for it. People notice 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.
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.
- Reads index.ts and the typed props
- Adds AvatarUpload to the settings page
- Writes the presign route on your server
- 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
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.
- DropUpload.zip
- REQUIREMENTS.mdwhat to install
- ts/DropUploadTypeScript source
- js/DropUploadthe same, plain JavaScript
- Drop area and file list
- Avatar with crop and zoom
- Five upload adapters
- Four languages
- Typed props and callbacks
- CSS Modules
- Your licence
DropUpload folder into your project.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
Launch price. $39 after .
- One developer
- Unlimited projects of your own, commercial included
- Lifetime updates
- 12 months of email support
Team
For teams, agencies and client work
Launch price. $119 after .
- Up to 10 developers
- Unlimited projects, client work included
- Lifetime updates
- 12 months of email support
Not sure which one? The FAQ answers it in two questions.



