How to build an accessible FileUploader in React

The drop zone is a real <button> so it is keyboard-operable (Enter/Space open the native file picker); it is named by the visible label via aria-labelledby (or by ariaLabel) and described by the hint only, so the name is the field rather than the generic drop instruction. Ids come from useId, so two uploaders on a page never collide. A single aria-live="polite" region is mounted unconditionally — a region added in the same commit as its first file announces nothing — and reports the file count plus the list-wide state. Per-file glyphs carry role="img" so their aria-label is honoured, a failed file's message is a role="alert", the file input stays in the accessibility tree (aria-hidden on a node that is programmatically focused and clicked is the aria-hidden-focus violation) and carries the field label as its own name, since an exposed form control without one fails the axe label rule, disabled reaches the remove controls too, and remove targets meet the 44px floor under a coarse pointer. Drag-over, disabled and error states each get a forced-colors treatment, since colour alone disappears there.

When to use a FileUploader

When not to use it

Keyboard interactions

Role button, verified at WCAG 2.2-AA.

Common mistakes

Avoid: Treating the component as uncontrolled and ignoring onFilesAdded

Prefer: Track accepted files in state and pass them back via the files prop

The file list is controlled — without wiring files/onFilesAdded/onRemove nothing renders in the list and status cannot update

Avoid: Relying only on accept/maxSize for security

Prefer: Re-validate type and size on the server after upload

Client-side accept/maxSize are UX filters, not enforcement; a determined user can bypass them

Example

<FileUploader onFilesAdded={console.log} />

See the full FileUploader reference →