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.

A dark file upload panel with a dotted drop zone, two files queued below it showing filenames, per-file progress bars, and small cancel buttons.
Drop zone, per-file progress, cancel on each row. The minimum that doesn't feel cheap.

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."

A full-page overlay appearing as a user drags a file over a dashboard, with a centered Drop to upload label.
Page-level drop catch. The whole app accepts the file.

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 modern pointerEvents: none trick.
  • 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:

  1. 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.
  2. 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.
  3. 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:

  1. 01Direct to your backend. Fine for small files and low volume. Becomes a bottleneck above a few MB or a few users.
  2. 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.
  3. 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