UnflowUI
Components

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. Pass uploadFn to actually perform the upload and get real progress/complete/error rows for free; omit it and every accepted file is simply marked complete, since this design system has no upload transport of its own.
  • Controlled — pass files yourself (an array of UploadFileEntry) and Upload still renders the rows, but never mutates the array; use onRemoveFile to 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

PropTypeDefaultDescription
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
titleReactNodetranslated copyMain call-to-action text
hintReactNodetranslated copySupporting hint text. Pass null to hide it entirely
dividerLabelReactNodetranslated copy ("or")Label shown between the click target and the button
buttonLabelstringtranslated copy ("Select files")Label for the built-in select-files button
acceptstringNative accept attribute, also used for validation (comma-separated extensions or MIME patterns, e.g. .pdf,image/*)
multiplebooleantrueAllows selecting/dropping more than one file
maxSizenumberMax size per file, in bytes
maxFilesnumberMax number of files accepted overall
currentFileCountnumber0Files already accepted elsewhere — combined with new selections when checking maxFiles
disabledbooleanDisables click-to-browse and drag-and-drop
onFilesAdded(files: File[]) => voidCalled with the files that passed validation, in both modes
onFilesRejected(rejections: { file: File; reason: 'accept' | 'maxSize' | 'maxFiles' }[]) => voidCalled with the files that failed validation, in both modes
filesUploadFileEntry[]Controlled file list. When provided, Upload renders a row per entry but never mutates the array itself
defaultFilesUploadFileEntry[]Seeds the internally-managed list. Ignored when files is provided
onFilesChange(files: UploadFileEntry[]) => voidFires whenever the internally-managed list changes. Not called when files is controlled
onRemoveFile(id: string) => voidCalled 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
hideFileListbooleanHides the built-in row list entirely, e.g. to render your own layout from onFilesChange/files
searchQuerystringFilters 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

PropTypeDefaultDescription
fileNamestringRequired.
fileSizenumberSize in bytes. Hidden from the row when omitted
fileTypestringMIME type, used to pick the row's default icon (generic file, image, video, or audio)
status'uploading' | 'complete' | 'error''complete'
progressnumber0-100. Only used when status is uploading. When omitted, an indeterminate spinner is shown instead of a progress ring
errorReactNodeDetailed error message, shown in a tooltip over the "Error" label. Only used when status is error
onRemove() => voidWires the trailing close button
disabledbooleanDisables the close button

Basic usage

Click to upload or drag and drop a file here

This is a hint text to help user.

or
<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.

or
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.

or

Disabled

Click to upload or drag and drop a file here

This is a hint text to help user.

or
<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.

Drag a file over this preview to see the full-screen dropzone.
<Upload presentation="fullScreen" uploadFn={uploadToServer} />

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

Error
<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.

or
<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

SlotTypeDescription
iconReactNodeOverrides the default paperclip icon
buttonReactNodeFull override of the built-in select-files button
fileIcon(entry: UploadFileEntry) => ReactNodeOverrides 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 PropPropsDescription
iconBaseHTMLProps<HTMLDivElement>The icon wrapper
titleBaseHTMLProps<HTMLParagraphElement>The title text
hintBaseHTMLProps<HTMLParagraphElement>The hint text
dividerBaseHTMLProps<HTMLDivElement>The "or" divider row
buttonOmit<IButton, 'onClick' | 'label'>The built-in select-files button
inputBaseHTMLProps<HTMLInputElement>The hidden native file input
dragPreviewChipOmit<ITag, 'label'>Every generic file-type chip shown while dragging over the dropzone
fileListBaseHTMLProps<HTMLDivElement>Wrapper around the built-in row list
fileItemOmit<IUploadItem, 'fileName' | 'fileSize' | 'fileType' | 'status' | 'progress' | 'error' | 'onRemove'>Applied to every row rendered from files/the internal list

UploadItem

SlotTypeDescription
iconReactNodeOverrides the default file-type icon
removeIconReactNodeOverrides the close icon
Slot PropPropsDescription
iconBaseHTMLProps<HTMLDivElement>The icon wrapper
fileNameBaseHTMLProps<HTMLParagraphElement>The file name text
metaBaseHTMLProps<HTMLDivElement>The trailing status/size/remove-button row
removeButtonOmit<IIconButton, 'icon' | 'onClick'>The close button
errorTooltipOmit<ITooltip, 'content' | 'anchorEl'>The tooltip wrapping the "Error" label

Accessibility

  • Upload's drag-and-drop is a progressive enhancement layered on top of a real Button, 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 a role="status" spinner; its determinate state renders Progress's role="progressbar" with aria-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 uploadFn is still in flight aborts the associated AbortSignal — pass it through to fetch/your HTTP client so the underlying request is actually cancelled, not just hidden.