File Upload
A drag-and-drop zone with a fallback file browser. Validates file count, type (via accept), and optional size limits. Selected files are listed below the zone with individual remove controls.
Preview
Drop files here or click to browse
PDF or images, max 5 MB each
States
Error
Drop files here or click to browse
Accepts .pdf
Please upload a PDF file.
Disabled
Drop files here or click to browse
Implementation
FileUpload.tsx
tsx
import { FileUpload } from "@/components/ui/FileUpload";
// Basic
<FileUpload
label="Attachment"
onFilesChange={(files) => console.log(files)}
/>
// Image only, max 5 MB
<FileUpload
label="Profile photo"
accept="image/*"
maxSize={5 * 1024 * 1024}
onFilesChange={(files) => setPhoto(files[0])}
/>
// Multiple files
<FileUpload
label="Documents"
accept=".pdf,.docx"
multiple
helperText="PDF or Word, max 10 MB each"
maxSize={10 * 1024 * 1024}
onFilesChange={setDocs}
/>
// Error state
<FileUpload
label="Resume"
error="Please upload a PDF."
accept=".pdf"
onFilesChange={setResume}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
accept | string | — | MIME types or file extensions (e.g. "image/*" or ".pdf,.docx"). Passed to the native input's accept attribute. |
multiple | boolean | false | Allows selecting more than one file. |
maxSize | number | — | Maximum file size in bytes. Files exceeding this are rejected with an inline error. |
onFilesChange | (files: File[]) => void | — | Called whenever the file list changes (add or remove). |
label | string | — | Visible label above the drop zone. |
helperText | string | — | Supplemental hint inside the drop zone. Defaults to showing accept and maxSize constraints. |
error | string | — | External error message shown below the drop zone. |
disabled | boolean | false | Prevents interaction and dims the control. |
ARIA roles
| Element | Role | Key attributes |
|---|---|---|
| Drop zone | button | tabIndex=0, aria-label='Upload files' |
| File input | (hidden) | aria-hidden='true' (activated programmatically) |
| File list | list | aria-label='Selected files' |
| Remove button | button (implicit) | aria-label='Remove {filename}' |
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Open the native file picker when the drop zone is focused |
| Tab | Move focus from the drop zone to individual remove buttons in the file list |
| Shift+Tab | Move focus backward through the file list and back to the drop zone |
| Enter / Space on remove | Remove the associated file from the list |
Accessibility
- →The drop zone uses role="button" with tabIndex so keyboard users can activate it with Enter or Space — triggering the hidden file input.
- →The actual <input type="file"> is sr-only and aria-hidden — it is activated programmatically. The visible drop zone is the interactive target.
- →Size and type errors appear as visible text; they are not announced via aria-live. If immediate announcement is needed, add role="alert" to the error element.
- →Each file in the list has an individual remove button with an aria-label describing the file being removed ("Remove resume.pdf").
- →The file list uses <ul> with aria-label="Selected files" so screen readers understand the context of the list items.