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
- Letting users attach one or more files via click-to-browse or drag-and-drop
- Showing per-file upload progress and status (uploading, complete, error) in a list
- Enforcing client-side accept/maxSize constraints with a callback for rejected files
When not to use it
- A single short text value — use Input
- Choosing from a fixed set of options rather than supplying a file — use Select or MultiSelect
Keyboard interactions
Role button, verified at WCAG 2.2-AA.
EnterSpace
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} />