File Upload

File Upload

A headless file picker with drag-and-drop, paste, validation and upload progress. Composes a dropzone or trigger button with a file list.

Playground

Drop files here or click to upload

File upload
accept
multiple
disabled
<FileUpload accept="image/*" multiple>
  <FileUploadDropzone>
    <ArrowUpToLineIcon className="size-6 stroke-1 text-muted-foreground" />
    <p className="pt-2 text-base font-medium text-foreground">
      Drop files here or click to upload
    </p>
    <FileUploadList>
      <FilesList />
    </FileUploadList>
  </FileUploadDropzone>
</FileUpload>

Preview

FileUploadDropzone accepts click, drop, paste and keyboard activation. Render accepted files inside FileUploadList by reading the store with useFileUpload and mapping to FileUploadItem.

Drop files here or click to upload

Any file, multiple allowed

File upload
<FileUpload multiple>
  <FileUploadDropzone>
    <ArrowUpToLineIcon className="size-6 stroke-1 text-muted-foreground" />
    <p className="pt-2 text-base font-medium text-foreground">
      Drop files here or click to upload
    </p>
    <p className="text-sm text-accent-foreground">
      Any file, multiple allowed
    </p>
    <FileUploadList>
      <FilesList /> {/* maps useFileUpload state to FileUploadItem */}
    </FileUploadList>
  </FileUploadDropzone>
</FileUpload>

Installation

npx shadcn@latest add https://espresso-base-ui.vercel.app/r/file-upload.json

Usage

import {
  FileUpload,
  FileUploadClear,
  FileUploadDropzone,
  FileUploadItem,
  FileUploadItemDelete,
  FileUploadItemMetadata,
  FileUploadItemPreview,
  FileUploadItemProgress,
  FileUploadList,
  FileUploadTrigger,
  useFileUpload,
} from "@/components/ui/file-upload"
function Files() {
  const fileStates = useFileUpload((state) => Array.from(state.files.values()))
  return (
    <>
      {fileStates.map((fs) => (
        <FileUploadItem key={fs.file.name} value={fs.file}>
          <FileUploadItemPreview />
          <FileUploadItemMetadata />
          <FileUploadItemDelete asChild>
            <Button variant="ghost" size="icon-sm">
              <Trash />
            </Button>
          </FileUploadItemDelete>
        </FileUploadItem>
      ))}
    </>
  )
}

<FileUpload multiple onValueChange={setFiles}>
  <FileUploadDropzone>
    <p>Drop files here</p>
    <FileUploadList>
      <Files />
    </FileUploadList>
  </FileUploadDropzone>
</FileUpload>

Trigger button

Skip the dropzone entirely: FileUploadTrigger opens the file picker from any button, and FileUploadClear removes every file (it only renders while files are present).

File upload
<FileUpload multiple maxFiles={5}>
  <div className="flex items-center gap-2">
    <FileUploadTrigger asChild>
      <Button variant="outline" size="sm">
        <UploadIcon />
        Browse files
      </Button>
    </FileUploadTrigger>
    <FileUploadClear asChild>
      <Button variant="ghost" size="sm">
        Clear all
      </Button>
    </FileUploadClear>
  </div>
  <FileUploadList>
    <FilesList />
  </FileUploadList>
</FileUpload>

Validation

accept, maxFiles and maxSize reject non-conforming files, briefly flagging the dropzone invalid; handle rejections in onFileReject (or run custom checks in onFileValidate).

Drop images here

PNG / JPG / GIF – up to 2 MB each, max 3 files

File upload
<FileUpload
  accept="image/*"
  maxFiles={3}
  maxSize={2 * 1024 * 1024}
  multiple
  onFileReject={(file, message) => toast.error(file.name + ": " + message)}
>
  <FileUploadDropzone>
    <ArrowUpToLineIcon className="size-6 stroke-1 text-muted-foreground" />
    <p className="pt-2 text-base font-medium text-foreground">
      Drop images here
    </p>
    <p className="text-sm text-accent-foreground">
      PNG / JPG / GIF – up to 2 MB each, max 3 files
    </p>
    <FileUploadList>
      <FilesList />
    </FileUploadList>
  </FileUploadDropzone>
</FileUpload>

Upload progress

Pass onUpload to start uploading as soon as files are accepted – report per-file progress through the provided callbacks and FileUploadItemProgress renders it as a linear bar, a circular ring or a fill overlay.

Drop files to upload

Fake upload simulates ~2s per file

File upload
<FileUpload
  multiple
  onUpload={async (files, { onProgress, onSuccess, onError }) => {
    await Promise.all(
      files.map((file) => uploadToServer(file, onProgress, onSuccess))
    )
  }}
>
  <FileUploadDropzone>
    <ArrowUpToLineIcon className="size-6 stroke-1 text-muted-foreground" />
    <p className="pt-2 text-base font-medium text-foreground">
      Drop files to upload
    </p>
    <FileUploadList>
      {/* items render <FileUploadItemProgress variant="linear" /> */}
      <FilesList progress="linear" />
    </FileUploadList>
  </FileUploadDropzone>
</FileUpload>

API reference

FileUpload is fully composable: the root owns the file store and a visually hidden input type="file", and every part supports asChild to merge onto your own element. Files work controlled (value + onValueChange) or uncontrolled (defaultValue).

FileUpload

PropTypeDefaultDescription
valueFile[]–Controlled list of files.
defaultValueFile[]–Initial files when uncontrolled.
onValueChange(files: File[]) => void–Called with the full file list whenever it changes.
onAccept(files: File[]) => void–Called once per selection with all files that passed validation.
onFileAccept(file: File) => void–Called for each accepted file.
onFileReject(file: File, message: string) => void–Called for each rejected file with the rejection reason.
onFileValidate(file: File) => string | null | undefined–Custom validator – return a message to reject the file.
onUpload(files, { onProgress, onSuccess, onError }) => Promise<void> | void–Upload handler run for accepted files; drive item progress via the callbacks.
acceptstring–Accepted types as a comma-separated list of MIME types, wildcards ("image/*") or extensions.
maxFilesnumber–Maximum number of files; extras are rejected.
maxSizenumber–Maximum size per file in bytes.
multiplebooleanfalseAllow selecting more than one file.
disabledbooleanfalseDisables the input, dropzone, trigger and clear parts.
invalidbooleanfalseMarks the upload invalid (styles the dropzone).
requiredbooleanfalseMarks the hidden file input as required.
namestring–Name of the hidden file input for form submission.
labelstring"File upload"Screen-reader label announced for the hidden file input.
dir"ltr" | "rtl"–Reading direction, inherited by all parts.
asChildbooleanfalseMerge root props onto the child element.

FileUploadList

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"Stack items vertically or lay them out as a wrapping row of tiles. Reflected as data-orientation.
forceMountbooleanfalseRender the list even while it has no files.

FileUploadItem

PropTypeDefaultDescription
valueFile–The file this item represents; state is looked up in the store.

FileUploadItemProgress

PropTypeDefaultDescription
variant"linear" | "circular" | "fill""linear"Progress rendering – bar, SVG ring, or a translucent fill over the item.
sizenumber40Diameter of the circular variant in pixels.
forceMountbooleanfalseKeep rendering after progress reaches 100.

FileUploadItemMetadata

PropTypeDefaultDescription
size"default" | "sm""default"Typography scale of the default name + size rendering; pass children to fully replace it.
ComponentDescription
FileUploadDropzoneClick / drag-and-drop / paste target (data-slot="file-upload-dropzone"). Exposes data-dragging, data-invalid and data-disabled.
FileUploadTriggerButton that opens the file picker (data-slot="file-upload-trigger").
FileUploadListrole="list" container for items; hidden until files exist (data-slot="file-upload-list").
FileUploadItemrole="listitem" card for one file with an sr-only status line (data-slot="file-upload-item").
FileUploadItemPreviewThumbnail – an object-URL image for image files, else a type-matched file icon (data-slot="file-upload-preview"). Accepts a render prop override.
FileUploadItemMetadataFile name, formatted size and any error message (data-slot="file-upload-metadata").
FileUploadItemProgressrole="progressbar" bound to the file's upload progress (data-slot="file-upload-progress").
FileUploadItemDeleteButton that removes its file from the store (data-slot="file-upload-item-delete").
FileUploadClearButton that removes all files; renders only while files exist unless forceMount (data-slot="file-upload-clear").
useFileUploadSelector hook over the store state ({ files, dragOver, invalid }) – use it inside FileUpload to render the item list.

Accessibility & styling hooks

The real input type="file" stays in the tree (visually hidden) so forms and assistive tech see a native control, labelled via aria-labelledby from the label prop. The dropzone is a focusable role="region" that opens the picker on Enter or Space and reflects aria-invalid and aria-disabled; items announce name, size and upload status through aria-describedby and progress uses a proper role="progressbar".

For styling, every part carries a data-slot attribute, and the dropzone additionally exposes data-dragging while a drag hovers it, plus data-invalid and data-disabled – target these from CSS for custom drop states.

Search documentation

Search the component docs