FileUploader
A drag-and-drop dropzone with a click-to-browse fallback. Enforces MIME / extension matching, file size bounds, and per-file count caps, surfaces a list of files with pending / uploading / done / error status and an animated progress bar. The component itself is presentational — the consumer drives the actual upload (fetch, XHR, SDK) and reflects progress back through controlled value.
Live Preview
How it works
- The user drops or selects files.
FileUploadervalidates each one againstaccept,maxSize,minSize,maxFiles, and the optionalvalidatecallback. - Accepted files are appended to the items list with
status: 'pending'andonFilesAddfires with the rawFileobjects. - The consumer kicks off the upload externally and updates each item’s
status/progressby passing a controlledvalue. - Rejected files are reported via
onRejectalong with a reason — never silently dropped.
Import
import { FileUploader } from 'entangle-ui';import type { FileUploaderItem } from 'entangle-ui';Usage
The simplest usage takes no value at all — the component manages the list internally. Useful for forms where you only need the final list at submit time.
<FileUploader label="Attachments" accept="image/*,.pdf" maxSize={5 * 1024 * 1024}/>For real uploads, control the list:
const [items, setItems] = useState<FileUploaderItem[]>([]);
<FileUploader value={items} onChange={setItems} onFilesAdd={files => uploadAll(files, setItems)} accept="image/*,.pdf" maxSize={5 * 1024 * 1024}/>;Single file mode
multiple={false} switches to single-file mode — selecting a new file replaces the previous one.
Single file
<FileUploader label="Avatar" accept="image/*" multiple={false} maxSize={2 * 1024 * 1024}/>Sizes
Sizes
<FileUploader size="sm" /><FileUploader size="md" /><FileUploader size="lg" />With simulated upload
A realistic end-to-end demo: drop a file, watch its row progress to done. Rejections surface below the dropzone.
Simulated upload
const [items, setItems] = useState<FileUploaderItem[]>([]);
<FileUploader accept=".pdf,.docx,image/*" maxFiles={5} maxSize={10 * 1024 * 1024} value={items} onChange={setItems} onReject={(file, reason) => toast.error(`${file.name}: ${reason}`)} onFilesAdd={files => { // Kick off the upload for each just-added file files.forEach(file => startUpload(file, setItems)); }}/>;The per-row UI shows the file name, formatted size, status badge, and animated progress bar — all driven from the FileUploaderItem shape:
interface FileUploaderItem { id: string; file: File; status: 'pending' | 'uploading' | 'done' | 'error'; progress?: number; // 0–100 errorMessage?: string;}Validation
Built-in checks for type, size, and count are available via the matching props. For anything beyond that, pass a synchronous validate callback — return false to reject silently, or a string to use as the rejection reason.
Validation
<FileUploader accept="image/*" maxSize={1 * 1024 * 1024} validate={file => { if (file.name.startsWith('.')) return 'no-dotfiles'; return true; }} onReject={(file, reason) => console.warn(file.name, reason)}/>| Reason | When |
|---|---|
'wrong-type' | File MIME or extension does not match accept. |
'too-large' | file.size > maxSize. |
'too-small' | file.size < minSize. |
'max-files' | The list already holds maxFiles items. |
'invalid' | The validate callback returned false. |
| custom | The string returned from a validate callback is forwarded as-is. |
Error state
When you need to surface a form-level error (e.g. the server rejected the batch), use the error / errorMessage props.
Error
<FileUploader label="Upload" error errorMessage="Could not upload — please try again."/>Disabled
Disabled
<FileUploader label="Locked" disabled />Accessibility
- Dropzone is keyboard-reachable as
role="button"withtabIndex={0}; Enter and Space open the native file picker. - Drag-over state is reflected visually and via
aria-busy="true". aria-invalidis set in the error state;aria-requiredwhenrequiredis set.aria-describedbylinks to the helper / error text.- Remove buttons on each row have a labelled
aria-label="Remove <filename>". - The hidden
<input type="file">carries thenameattribute when supplied, so native form submission still works for simple use cases.
API Reference
<FileUploader>
| Prop | Type | Default | Description |
|---|---|---|---|
value | FileUploaderItem[] | — | Controlled list of items. |
defaultValue | FileUploaderItem[] | — | Default list (uncontrolled). |
onChange | (items: FileUploaderItem[]) => void | — | Called whenever the items list changes. |
onFilesAdd | (files: File[]) => void | — | Called with the raw `File` objects that were just accepted (after validation, before they appear as `pending`). Kick off the upload here. |
onFileRemove | (item: FileUploaderItem) => void | — | Called when a single item is removed. |
onReject | (file: File, reason: FileUploaderRejectReason) => void | — | Called for files rejected by validation. |
accept | string | — | Comma-separated list of accepted MIME types and/or extensions (e.g. `image/*,.pdf`). All types when omitted. |
multiple | boolean | true | Allow selecting more than one file at a time. |
maxFiles | number | — | Maximum number of files the uploader will hold. Extra files are rejected as `max-files`. |
maxSize | number | — | Maximum file size in bytes. Files above are rejected as `too-large`. |
minSize | number | — | Minimum file size in bytes. Files below are rejected as `too-small`. |
validate | (file: File) => boolean | string | — | Synchronous validator. `false` rejects as `invalid`, a string is forwarded as the rejection reason. |
renderItem | (state: FileUploaderRenderItemState) => ReactNode | — | Custom row renderer. When omitted the built-in row is used. |
size | 'sm' | 'md' | 'lg' | 'md' | Component size. |
label | string | — | Label rendered above the dropzone. |
helperText | string | — | Helper text rendered below. |
error | boolean | false | Error state. |
errorMessage | string | — | Error message — overrides `helperText` when `error` is true. |
disabled | boolean | false | Disable interaction with the uploader. |
required | boolean | false | Mark the field as required. |
dropZoneText | ReactNode | 'Drag files here or click to browse' | Primary text shown inside the dropzone. |
dropZoneHint | ReactNode | — | Secondary text shown inside the dropzone. |
name | string | — | Hidden form-input name attribute (for native form submission). |
className | string | — | Additional CSS class names. |
style | CSSProperties | — | Inline styles. |
testId | string | — | Test identifier for automated testing. |
ref | Ref<HTMLDivElement> | — | Ref to the root element. |
FileUploaderItem
| Prop | Type | Default | Description |
|---|---|---|---|
id * | string | — | Stable identifier — generated for new items, preserved across renders. |
file * | File | — | Underlying `File` object. |
status * | 'pending' | 'uploading' | 'done' | 'error' | — | Current status of the upload pipeline. |
progress | number | — | Upload progress, 0–100. Optional in `pending` / `done`. |
errorMessage | string | — | Error message displayed when `status === 'error'`. |
FileUploaderRejectReason
'too-large' | 'too-small' | 'wrong-type' | 'max-files' | 'invalid' | string
Built-in reasons plus any string returned from a validate callback.