David Maillo
The File Uploader the Web Deserves
AI lets us build more than ever. It can also help us build better, if we point it at the small pieces nobody has time for. The file uploader is one of those pieces.
More, and faster
Something changed in how we build software. A feature that took a week takes an afternoon. A prototype that took a month takes a weekend. With an AI agent next to you, the cost of making one more thing has dropped close to zero.
Most of that new speed goes into volume. More features, more screens, more products. That's fine. Shipping things people want is the job.
But there's another way to spend it.
Or better
The same tools that let us build more can help us build things properly. The second pass nobody had time for. The edge case you knew about and never fixed. The animation that makes a moment feel right. AI doesn't only write the first draft. It's very good at finding what's wrong with one, and at helping you polish it until nothing is.
Attention to detail used to be a luxury, because it cost time you didn't have. It costs a lot less now. What's missing is someone deciding to spend it.
The piece every sprint rushes
Think about how many places an app ends up needing uploads. The profile picture. Attachments in a message. Documents in a form. A CSV import. A logo in the settings page. It keeps coming back.
And in every sprint, it gets the same treatment. It hides inside another ticket. It gets half a day. Someone builds the minimum: an input, a request, a success message. It works with the three files on the developer's desktop, passes review, and never gets touched again. Nobody stops to make a good one, because there's always something bigger to build.
The minimum ships with the same holes every time:
- A folder dropped on the box turns into a row that can only fail.
- An .exe renamed to .png gets straight through, because nobody reads the file's first bytes.
- Picking the same file twice does nothing, because the input still holds the old value.
- The instructions say "PNG or JPG, up to 5 MB" while the validation, edited months later, checks something else.
- Cancel doesn't cancel, or a late answer from the server marks the file as done after the user stopped it.
- Retry pressed twice runs two requests against one progress bar.
- File names with
#or?break the storage URL. - Paste attaches the wrong things, like an image pasted into the comment box next door, or the same screenshot in two uploaders at once.
- Videos get a broken image as their thumbnail.
- No keyboard, no screen reader, no reduced motion.
And it looks like the minimum
The bugs are half of it. The other half is how it feels.
The minimum uploader is a grey dashed rectangle and a "Choose file" button. No drag state, no progress, no sign that anyone thought about the moment. And it's a moment that matters. The upload box is often in the first minutes of a product: the profile picture during onboarding, the first document a customer sends you. It's one of the few places where people hand your app something of their own.
When that moment feels careless, people notice, even if they couldn't tell you why. When it feels good, they notice that too.
So I stopped and built a good one
Drop Upload is what happens when someone gives the uploader the time a sprint never does. I designed and built it by hand, then used AI the way I described above: to hunt down edge cases, question every state, and harden each fix. The result is a drag and drop file upload component for React that has already been through every bug on that list, with the source explaining each fix in its comments.
Validation that matches its own copy
You set allowedFormats, maxFileSize and maxFiles once. The same props
drive the checks and the instruction text, so what the box says and what it
enforces can't drift apart. It reads each file's first bytes to catch a
renamed file, turns folders away with a clear reason, and reports every
rejection to your code through onFilesRejected.
These checks save people from waiting on an upload that was never going to work. Your server should still validate again, and the docs say so.
Uploads that go where your files already live
Five adapters come in the box: your own REST
endpoint, S3, S3
multipart for large files,
Supabase and
Cloudinary. Each one has real progress, cancel
and retry. Every file gets its own AbortController, so cancelling one leaves
the rest of the queue alone.
An avatar uploader in the same box
AvatarUpload turns the drop area into a circle, with crop and zoom inside
it before anything uploads. It shares the same engine as the main component,
so validation, adapters, callbacks and languages behave the same in both.
The details that never make the roadmap
Paste to upload with Ctrl+V. Hold to discard, so nobody loses a file by accident. Real video thumbnails. A stacked list that doesn't push your form down the page. Full keyboard support. Every animation respects reduced motion. English, Spanish, French and German built in, and every string can be reworded. Light and dark themes, passed in as a prop so it renders the same on the server and never flashes.
And a small puff of smoke where a file lands. Nobody asks for it. People notice anyway.
Why it fits a React app
Drop Upload is written the way React code is meant to be written. You describe the uploader with props and hear from it through callbacks. You never touch a DOM node or wire up a drag listener yourself.
import { DropUpload, createRestAdapter } from "./components/DropUpload"
const adapter = createRestAdapter({ endpoint: "/api/upload" })
export default function Page() {
return (
<DropUpload
allowedFormats={["image/*", "application/pdf"]}
maxFileSize={5 * 1024}
maxFiles={3}
uploadAdapter={adapter}
onUploadComplete={(file) => console.log(file.url)}
/>
)
}When your app needs to be in charge, a ref gives you addFiles,
removeFile, clearFiles, startUpload and cancelUpload. Set
autoUpload={false} and files wait until the form is submitted.
- It works with React 18 and 19, in Next.js, Vite, Remix and Astro. It
ships its own
"use client"directive, so it imports straight into a Server Component page. - It's built on the browser's own File, DataTransfer and AbortController
APIs. React and
motionare the only things it needs. That's one less package in your supply chain to audit, update and wait on. - It won't fight your styles. Every colour is a
--du-*CSS variable, and the rest is plain CSS Modules. It sits happily next to Tailwind or anything else. - The code is yours. No npm package to pin, no black box. If you want it to behave differently, open the file and change it.
Spend the speed on something
Your AI agent can write an upload box in a minute. That's the minimum again, just faster. Give it Drop Upload instead, and it spends that minute wiring a finished component into your app. The docs have prompts that work.
Then use the time you saved on the next small piece that deserves better.
Throw some files at the demo above, or open the playground and change every prop. The docs cover setup, adapters and customisation, and the pricing page has the rest.