Skip to content

File Upload

The control for getting a file from a person's machine into a product — and the place where most products lose the work silently.

UI Components

A file upload gets a file from somebody's machine into the product. It is a small component with an unusually high cost of failure, because the thing being lost is work that already exists.

Two routes, both required

A button that opens the native file dialog. This is the one that has to exist: reachable by keyboard, labelled, usable by anybody.

A drop zone for dragging files in. Faster when it fits, and unusable by keyboard, awkward on touch, and irrelevant to anybody whose file is not visible on screen behind the browser window.

Products that ship only the drop zone have removed the accessible path in favour of the convenient one.

Validate before the bytes move

Check type and size on selection. A 200MB file on a slow connection should fail in a second, not after four minutes of upload.

Say the limits before the attempt, not in the error. "PDF or PNG, up to 10MB" above the control costs one line and prevents the failure.

Server-side validation still runs. The client check is a courtesy; it is not a security control, and anything that treats it as one has a hole.

Progress

The total size is always known, so this is a progress bar, not a spinner.

Per file, and overall. With twelve files, one failure is invisible inside a single aggregate bar.

Cancel has to cancel, aborting the request rather than hiding the row.

Say whether closing the page kills it. People will close the page.

Failure in a batch

The expensive mistake: one file is the wrong type, the whole batch is discarded, eleven successful uploads are gone.

Keep what succeeded. Mark the failure inline with a reason specific enough to act on — "PNG and PDF only" beats "invalid file" — and offer a retry on that row alone. See error state.

After the upload

The component does not end at 100%.

Show what is there with a name, size, and where it is meaningful a thumbnail, so somebody knows the right file landed.

Allow removal, and decide deliberately whether removal is immediate or takes effect on save. Both are defensible; ambiguity is not.

Handle the duplicate name. Replacing a file silently because the name matched is a data loss event that looks like success.

Accessibility

Keep the real input. The common implementation hides <input type="file"> behind a styled div and loses the keyboard path. Style the label instead.

Announce state through a live region — selected, uploading, complete, failed.

Name each row with its file and status, so a list of eight uploads is navigable rather than eight instances of the word "file".

In practice

An onboarding step asks for a company logo. The drop zone is the only control, sized as a large dashed rectangle, and the accepted formats appear in the error message after a failed attempt.

Support sees the same ticket repeatedly: somebody uploads a logo, nothing appears to happen, they try three more times, then write in. The file was a 12MB TIFF, over the limit, and the error rendered below the fold on a laptop.

Three changes close it. The limits appear above the control. A button sits beside the drop zone. And the error renders inside the drop zone, where the person is already looking.

The upload code never changed.

Where teams get it wrong

  • Drag and drop alone. The accessible route removed.
  • Limits stated only in errors. A rule people learn by failing.
  • A spinner. A known total, reported as unknown.
  • A whole batch discarded. Eleven successes thrown away with one failure.
  • A hidden native input. No keyboard path.
  • Silent overwrite on a duplicate name. Data loss that looks like success.

Frequently Asked Questions