File uploaders that don't feel cheap
Drag-drop, paste, progress, retry. The five elements every file upload needs, and the small touches that separate a real product from a wrapper around a file input.
A file uploader is one of those components that looks done after two hours of work and is broken for six months after that. The minimum, an <input type="file"> styled as a button, works. It also signals we didn't think about this, which is exactly the wrong signal in a product where uploading is core (design tools, support inboxes, document apps, AI chat). The uploader is the first impression of every flow that starts with bring-your-own-file.

There's a small set of elements that turn a bare input into something users actively enjoy using. None of them are hard. Most teams ship two of the five and miss the rest.
The five elements that matter
1. Drag and drop, but anywhere
Not just a small dotted rectangle. The whole drop zone should accept files, and dropping anywhere on the page should trigger the same upload. Notion does this; Linear does this; Figma does this. The page-level drop is the difference between "upload a file" and "drop a file in the app."

When a drag enters the window, the entire surface shows a soft overlay ("Drop to upload"). When the drag leaves, the overlay fades out. The drop zone in the form is still the primary affordance for users who don't drag; the page-level catch is the power-user shortcut.
2. Paste support
Ctrl/Cmd+V should attach an image from the clipboard. Screenshot, then paste, is the fastest path from "I want to share this" to "it's uploaded." The clipboard API makes this two lines of code. Most products skip it. Don't be most products.
3. Real progress, per file
Each upload shows its filename, a progress bar, current speed ("2.4 MB/s"), and a cancel button. Not a single overall progress bar across all files, per-file is the standard now because users want to know which one is stuck. Show the progress bar even on small files; the visual confirmation matters more than the time it shows.
4. Retries that don't lose the file
If an upload fails, show the file with an error state and a one-click retry. Never silently drop the file. Never make the user re-select it. The retry should also work if the user closes the tab and comes back, resumable uploads (tus.io, AWS multipart) are the production answer here.
5. Previews
Images get thumbnails. PDFs get a small icon with the filename. Videos get a poster frame after upload finishes. The preview tells the user the file made it, which the progress bar alone doesn't quite communicate.
The composition rules
- Show file size and accepted types up front. "Drop or click to upload, JPG, PNG, PDF up to 25MB." Users who get rejection errors after upload feel tricked.
- Validate client-side before transmitting. File type, file size, count. Showing an error before a megabyte travels saves both time and bandwidth.
- Multiple files queue, they don't replace. Adding a second file shouldn't drop the first. Always.
- Drag-drop indicator stays during the drag, not just the dragenter. The most common drag-drop bug is the drop zone losing its highlight when the user passes over a child element. Use
dragover+ a counter, or the modernpointerEvents: nonetrick. - Remove file after upload is a real action, with a confirmation if the file is required. Accidental deletion of an upload is the second-most-common support ticket for upload-heavy products.
Where to put the uploader
Three placements, depending on the flow:
- 01Inline in a form. The upload is one of several fields. The drop zone is part of the form's layout; the page-level drop catch can still apply.
- 02A standalone page or modal. The upload is the entire task. The drop zone takes most of the viewport. Used by image hosts, AI products that take a file as input, document storage.
- 03Floating attach button. Inside an editor or chat, the upload is one of several input modes. A paperclip icon opens a small file menu (or just triggers the file dialog directly).
Error states that don't blame the user
Most upload errors are not the user's fault, network blip, server timeout, transient cloud storage error. The error message should reflect that:
- "Upload failed. Retry?" beats "Error: file rejected." The first is a recovery path; the second is a dead end.
- Validation errors get specifics. "PDF is 28MB, max is 25MB" beats "File too large." The user can act on the first.
- Type errors offer a workaround when one exists. "PNG isn't supported, convert to JPG?" or a link to a converter. Most teams stop at the rejection; the helpful 5% retain the user.
- Server errors get a logged ID the user can mention if they contact support. "Error #af3b2c, retry or copy this ID for support."
The infrastructure choices
Three tiers, depending on scale:
- 01Direct to your backend. Fine for small files and low volume. Becomes a bottleneck above a few MB or a few users.
- 02Pre-signed URLs to S3/R2/Blob. Client uploads directly to object storage; backend signs the URL and verifies the result. The default for production SaaS in 2026.
- 03Resumable / chunked uploads. tus.io is the open standard; Cloudflare Stream and AWS multipart do equivalents. Required for files over ~100MB or for users on flaky connections.
Frequently asked
Drag-drop or click-to-upload, which is better?
Both, always. Click is the default for users who don't think to drag, drag is the power-user path, and they cost nothing to ship together. A drop zone that's also a click target is the universal pattern.
Should I show upload speed?
Yes, in MB/s or similar. Users can't act on the number, but seeing motion makes the wait feel shorter. Add an ETA on uploads over 30 seconds ("~12s remaining") for the same reason.
What about cancel and pause?
Cancel is required. Pause is a luxury, ship it only if your users routinely upload large files on shared bandwidth. For 95% of products, cancel-and-restart is enough.
How do I handle huge files?
Resumable uploads via tus or multipart, chunked at 5–25MB per piece. Show overall progress and per-chunk progress. Persist the upload state so a refresh doesn't lose the user's progress. Without this, every large-file upload is a coin flip on whether it survives a network blip.
Ship one
The forms entry in the directory has a tuned uploader prompt with drag-drop, paste, per-file progress, and retry already wired. Pair with the interaction entry for the keyboard and focus rules, and the empty states entry for the "no files yet" zone that uploaders share with the rest of your product.
Keep reading
Forms that feel fast (even when they're not)
Inline validation, field grouping, the multi-step pattern, and the small details that turn a 12-field form into something users actually finish.
Loading states beyond the spinner
Skeletons, optimistic UI, progress bars. The four loading patterns worth shipping, and the latency thresholds that decide which one.