Upload
A click-to-browse / drag-and-drop file dropzone, paired with a file-row component for uploading, complete, and error states.
Overview
Upload renders the dropzone — a click-to-browse box that also accepts drag-and-drop — and automatically renders the resulting file list as UploadItem rows underneath it. It works two ways:
- Uncontrolled (default) — just drop
<Upload />in and it manages its own file list. PassuploadFnto actually perform the upload and get real progress/complete/error rows for free; omit it and every accepted file is simply markedcomplete, since this design system has no upload transport of its own. - Controlled — pass
filesyourself (an array ofUploadFileEntry) and Upload still renders the rows, but never mutates the array; useonRemoveFileto react to the remove button and update your own state.
Either way, UploadItem (the row) stays fully presentational and can also be used completely standalone.
Import
import { Upload } from '@unflow.io/ui/components/Upload';
import { UploadItem } from '@unflow.io/ui/components/UploadItem';Upload props
| Prop | Type | Default | Description |
|---|---|---|---|
presentation | 'inline' | 'fullScreen' | 'inline' | inline scopes drag-and-drop to this element. fullScreen listens for file drags anywhere on the page and shows the dropzone as a full-viewport overlay while one is in progress |
title | ReactNode | translated copy | Main call-to-action text |
hint | ReactNode | translated copy | Supporting hint text. Pass null to hide it entirely |
dividerLabel | ReactNode | translated copy ("or") | Label shown between the click target and the button |
buttonLabel | string | translated copy ("Select files") | Label for the built-in select-files button |
accept | string | — | Native accept attribute, also used for validation (comma-separated extensions or MIME patterns, e.g. .pdf,image/*) |
multiple | boolean | true | Allows selecting/dropping more than one file |
maxSize | number | — | Max size per file, in bytes |
maxFiles | number | — | Max number of files accepted overall |
currentFileCount | number | 0 | Files already accepted elsewhere — combined with new selections when checking maxFiles |
disabled | boolean | — | Disables click-to-browse and drag-and-drop |
onFilesAdded | (files: File[]) => void | — | Called with the files that passed validation, in both modes |
onFilesRejected | (rejections: { file: File; reason: 'accept' | 'maxSize' | 'maxFiles' }[]) => void | — | Called with the files that failed validation, in both modes |
files | UploadFileEntry[] | — | Controlled file list. When provided, Upload renders a row per entry but never mutates the array itself |
defaultFiles | UploadFileEntry[] | — | Seeds the internally-managed list. Ignored when files is provided |
onFilesChange | (files: UploadFileEntry[]) => void | — | Fires whenever the internally-managed list changes. Not called when files is controlled |
onRemoveFile | (id: string) => void | — | Called when a row's remove button is clicked, with that entry's id |
uploadFn | (file: File, helpers: { onProgress: (percent: number) => void; signal: AbortSignal }) => Promise<void> | — | Uncontrolled mode only — actually perform the upload. Report progress via onProgress; signal aborts if the row is removed mid-upload. Omit it to just mark every accepted file complete |
hideFileList | boolean | — | Hides the built-in row list entirely, e.g. to render your own layout from onFilesChange/files |
searchQuery | string | — | Filters the rendered rows to those whose file name matches (case-insensitive), without touching the underlying list |
interface UploadFileEntry {
id: string;
file: File;
status: 'uploading' | 'complete' | 'error';
progress?: number;
error?: ReactNode;
}UploadItem props
| Prop | Type | Default | Description |
|---|---|---|---|
fileName | string | — | Required. |
fileSize | number | — | Size in bytes. Hidden from the row when omitted |
fileType | string | — | MIME type, used to pick the row's default icon (generic file, image, video, or audio) |
status | 'uploading' | 'complete' | 'error' | 'complete' | |
progress | number | — | 0-100. Only used when status is uploading. When omitted, an indeterminate spinner is shown instead of a progress ring |
error | ReactNode | — | Detailed error message, shown in a tooltip over the "Error" label. Only used when status is error |
onRemove | () => void | — | Wires the trailing close button |
disabled | boolean | — | Disables the close button |
Basic usage
Click to upload or drag and drop a file here
This is a hint text to help user.
<Upload />Uncontrolled — batteries included
Pass uploadFn and Upload does the rest: picked/dropped files show up as rows immediately, each one's progress ring is driven by your onProgress calls, and removing a row aborts its signal. No state to wire up yourself.
Click to upload or drag and drop a file here
This is a hint text to help user.
function uploadToServer(file: File, { onProgress, signal }: {
onProgress: (percent: number) => void;
signal: AbortSignal;
}) {
return fetch('/api/upload', { method: 'POST', body: file, signal }).then(() => {
onProgress(100);
});
}
<Upload maxSize={5 * 1024 * 1024} uploadFn={uploadToServer} />Controlled — you own the list
Pass files instead, and Upload still renders the rows for you — it just never mutates the array. Use onFilesAdded to append and onRemoveFile to remove, exactly like any other controlled input.
Click to upload or drag and drop a file here
This is a hint text to help user.
Disabled
Click to upload or drag and drop a file here
This is a hint text to help user.
<Upload disabled />Full-screen drop target
Set presentation="fullScreen" to listen for file drags anywhere on the page instead of just the component's own box. No persistent box is rendered — dragging a file over the window reveals the dropzone centered in a full-viewport overlay (built on top of the existing Backdrop component, and centered the same way Modal's center placement is), and it disappears again on drop, drag-cancel, or Escape. Since there's no click target to reach in this mode, the divider and "Select files" button are omitted — only the icon, title, and hint show. The row list still renders normally wherever Upload sits in your layout, and persists after the overlay closes.
<Upload presentation="fullScreen" uploadFn={uploadToServer} />Inside a Popper, with search
Upload is just a regular block-level component, so it composes directly with Dropdown (built on Popper) for a dropdown-style upload panel — reusing the same header pattern SelectDropdown uses for search: the field itself stays hidden behind an IconButton (a magnifying glass) and only appears once toggled, rather than sitting open by default. Wire the Input's value straight into Upload's searchQuery prop to filter the visible rows by file name without touching the underlying list.
File row states
file_example.pdf
45% uploaded...
1.0 MB
video_example.mov
3.0 MB
image_example.jpg
<UploadItem
fileName="file_example.pdf"
fileSize={1_048_576}
fileType="application/pdf"
status="uploading"
progress={45}
/>
<UploadItem fileName="video_example.mov" fileSize={3_145_728} fileType="video/quicktime" />
<UploadItem
fileName="image_example.jpg"
fileType="image/jpeg"
status="error"
error="The file exceeds the 5 MB size limit."
/>Indeterminate progress
When a file is uploading but no progress value is available to measure, omit the prop entirely — UploadItem falls back to an indeterminate spinner instead of a progress ring.
file_example.pdf
0% uploaded...
1.0 MB
Custom icons per file
UploadItem used on its own already accepts slots.icon to override its icon outright. When Upload is rendering the built-in row list for you, use slots.fileIcon instead — it's called once per row with that row's UploadFileEntry, so you can key off the file's name, type, or anything else you know about it and swap in a custom icon (or an image thumbnail) for just that file, while every other row keeps the automatic file/image/video/audio icon.
Click to upload or drag and drop a file here
This is a hint text to help user.
<Upload
slots={{
fileIcon: (entry) =>
entry.file.type.startsWith('image/') ? (
<img src={URL.createObjectURL(entry.file)} className="h-4 w-4 rounded object-cover" />
) : undefined, // fall back to the default icon for everything else
}}
/>Customization (slots & slotProps)
Upload
| Slot | Type | Description |
|---|---|---|
icon | ReactNode | Overrides the default paperclip icon |
button | ReactNode | Full override of the built-in select-files button |
fileIcon | (entry: UploadFileEntry) => ReactNode | Overrides a row's icon on a per-file basis — return undefined for a given entry to fall back to UploadItem's automatic MIME-based icon for that row |
| Slot Prop | Props | Description |
|---|---|---|
icon | BaseHTMLProps<HTMLDivElement> | The icon wrapper |
title | BaseHTMLProps<HTMLParagraphElement> | The title text |
hint | BaseHTMLProps<HTMLParagraphElement> | The hint text |
divider | BaseHTMLProps<HTMLDivElement> | The "or" divider row |
button | Omit<IButton, 'onClick' | 'label'> | The built-in select-files button |
input | BaseHTMLProps<HTMLInputElement> | The hidden native file input |
dragPreviewChip | Omit<ITag, 'label'> | Every generic file-type chip shown while dragging over the dropzone |
fileList | BaseHTMLProps<HTMLDivElement> | Wrapper around the built-in row list |
fileItem | Omit<IUploadItem, 'fileName' | 'fileSize' | 'fileType' | 'status' | 'progress' | 'error' | 'onRemove'> | Applied to every row rendered from files/the internal list |
UploadItem
| Slot | Type | Description |
|---|---|---|
icon | ReactNode | Overrides the default file-type icon |
removeIcon | ReactNode | Overrides the close icon |
| Slot Prop | Props | Description |
|---|---|---|
icon | BaseHTMLProps<HTMLDivElement> | The icon wrapper |
fileName | BaseHTMLProps<HTMLParagraphElement> | The file name text |
meta | BaseHTMLProps<HTMLDivElement> | The trailing status/size/remove-button row |
removeButton | Omit<IIconButton, 'icon' | 'onClick'> | The close button |
errorTooltip | Omit<ITooltip, 'content' | 'anchorEl'> | The tooltip wrapping the "Error" label |
Accessibility
Upload's drag-and-drop is a progressive enhancement layered on top of a realButton, which stays keyboard-operable and screen-reader visible on its own — the surrounding clickable box is a mouse-only convenience, not a duplicate interactive control.- Because browsers never expose a dragged file's name until it's dropped, the chips shown while dragging over the dropzone use generic type labels ("File", "Image", "Video", "Audio") derived from the drag payload's MIME type, rather than real file names.
UploadItem's indeterminate state renders arole="status"spinner; its determinate state rendersProgress'srole="progressbar"witharia-valuenow— assistive tech gets an accurate reading either way.- The "Error" label is wrapped in a
Tooltip, so the underlying error message is reachable by keyboard focus, not just mouse hover. - Removing a row while its
uploadFnis still in flight aborts the associatedAbortSignal— pass it through tofetch/your HTTP client so the underlying request is actually cancelled, not just hidden.
CardInput
A credit/debit card number field that auto-formats digits into groups and detects the card brand (Visa, Mastercard, Amex, and more) as the user types.
VerificationCode
A segmented one-time-code input — one cell per digit, with auto-advance, paste distribution, arrow-key navigation, and OTP autofill support built in.