Upload Composition

Drag and drop, with a real file input underneath. Dropping is an addition, never the only way in — a drop zone that only accepts drops is unusable by keyboard and unreachable on a phone.

Usage

PDFs only, up to 2 MB each, three at most.

tsx
<Upload  files={files}  onFilesChange={setFiles}  onReject={(rejections) => notify(rejections)}  accept={['.pdf']}  maxSize={2 * 1024 * 1024}  maxFiles={3}/>

One target, one behaviour

The zone is a <label> wrapping the file input, so the whole area opens the picker exactly once and the keyboard focus ring lands on the control itself. The earlier arrangement — a button beside a hidden input floating inside the zone — meant the picker could also be opened by clicking what looked like empty space.

Rejections are reported, not swallowed

Every rejected file comes back with a reason — type, size, or count — through onReject. A file that disappears without explanation reads as a broken uploader, and the person tries again with the same file.

Do

Say the limits in the hint before the drop, and surface rejections after it.

Don't

Filter silently on accept alone; a dropped file that never appears looks like data loss.

Props

PropTypeDefaultNotes
files / onFilesChangeFile[] / (files) => voidControlled. The component holds no list of its own.
onReject(rejections) => voidEach carries the file and a reason.
acceptstring[]Extensions or MIME types: ['.pdf', 'image/*'].
maxSizenumberBytes, per file.
maxFilesnumberCounted against what is already held.
multiplebooleantruefalse behaves as a limit of one.