Skip to Content
Puzzle

File Upload

Select, validate, and upload files with a keyboard-accessible dropzone.

File Upload supports native file selection, drag and drop, client-side validation, and optional automatic uploads. Without an onUpload handler, accepted files are listed as Ready to upload; the component does not send them anywhere.

Import

import { FileUpload, type FileUploadUploadHandler } from "@puzzle-ui/ui";

Basic usage

Click the dropzone, press Enter or Space while it is focused, or drop a file onto it. Use title, description, and browseLabel to explain what to select.

390 × 440

Single-file mode is the default: a new accepted file replaces the previous selection and aborts its in-progress upload. A rejected file does not replace the current selection. Each listed file has a Remove button.

Multiple files and selection changes

Set multiple to append accepted files across selections. onChange receives the entire accepted File[] whenever files are added, replaced, or removed, including [] after removing the last file.

The file list is managed internally; there are no controlled value or defaultValue props. Upload progress and completion do not call onChange.

390 × 560

Accepted types and limits

accept is an array of MIME types such as "application/pdf" or wildcards such as "image/*". File extensions such as ".png" are not supported by the component's validator. Files with an empty MIME type are rejected when an accept filter is configured.

maxFileSize limits each file, maxTotalSize limits the combined selected size, and maxFiles limits the count in multiple mode. Sizes are in bytes. Files are evaluated in selection order: valid files that fit are accepted, while rejected files are reported separately.

390 × 580

Rejections appear in an alert with the filename and reason. onReject receives the rejected files for that selection. New selections replace previous rejection messages; removing a selected file does not clear them.

<FileUpload
  accept={["image/png"]}
  onReject={(rejections) => {
    for (const { file, code, message } of rejections) {
      console.warn(file.name, code, message);
    }
  }}
/>

These are client-side checks for user feedback, not a security boundary. Validate file contents, types, sizes, and permissions again on your server.

Automatic uploads, progress, and retry

Provide onUpload to start uploading each accepted file automatically. The handler receives the file and { signal, onProgress }. Report progress from 0 to 100; resolve the promise on success and reject it on failure. Error messages are displayed alongside a Retry button, which calls the handler again for that file.

The following local simulation intentionally fails each file's first attempt. Select a file to see progress and error feedback, then retry to complete it. No file data is sent anywhere.

390 × 640

Files upload independently. Progress is clamped to 0-100, and resolving the handler marks the file complete at 100. Removing a file, replacing it in single mode, or unmounting aborts its signal; pass that signal to your network client to cancel the actual request. Late results from aborted attempts are ignored.

Connect your upload endpoint

Use your application's endpoint and handle unsuccessful HTTP responses explicitly. fetch does not provide upload-progress events; omit intermediate progress or use a client that supports them.

const uploadFile: FileUploadUploadHandler = async (file, { signal }) => {
  const body = new FormData();
  body.append("file", file);

  const response = await fetch("/api/uploads", {
    method: "POST",
    body,
    signal,
  });

  if (!response.ok) {
    throw new Error("Upload failed (" + response.status + "). Please retry.");
  }
};

<FileUpload multiple onUpload={uploadFile} />;

Labels and validation state

Wrap the component in FormField for an external label, supporting text, and validation message. Its label targets the native file input. The supplied description and error relationships are preserved alongside File Upload's own description.

required sets the native file input's required attribute. It does not validate the internally managed file list: the native input is cleared after selection so the same file can be selected again. For form submission, validate the files received by onChange and send them explicitly using FormData or your upload handler. Do not rely on name and native form serialization to submit the displayed file list.

390 × 440

Disabled state

disabled prevents file selection, dropping, removal, and retry, and removes the dropzone from keyboard navigation. It does not cancel uploads that are already running.

390 × 360

API

FileUpload

PropTypeDefaultDescription
acceptreadonly string[][]Accepted MIME types or MIME wildcards; empty accepts all types.
multiplebooleanfalseAppends files instead of replacing the selection.
maxFilesnumber-Maximum selected count in multiple mode.
maxFileSizenumber-Maximum bytes per file.
maxTotalSizenumber-Maximum combined selected bytes.
onChange(files: File[]) => void-Full accepted list after additions, replacement, or removal.
onReject(rejections: FileUploadRejection[]) => void-Files rejected from the latest selection.
onUploadFileUploadUploadHandler-Asynchronous upload handler; enables automatic uploading.
titlestring"Drop files here"Dropzone heading and default accessible name.
descriptionstring"Drag and drop your files here or browse to select them."Supporting dropzone text.
browseLabelstring"Browse files"Visible picker action text.
disabledbooleanfalseDisables user interaction.
requiredbooleanfalseNative input required attribute; see form limitations above.
namestring-Native input name; does not serialize the managed list.
idstringGeneratedNative file input ID for label association.
classNamestring-Additional class on the outer div.
styleCSSProperties-Styles on the outer div.
refRef<HTMLDivElement>-Ref to the outer div, not the native input.
aria-labelstringtitleAccessible name of the focusable dropzone.
aria-describedbystring-External description IDs, merged with the internal description.
aria-invalidAriaAttributes["aria-invalid"]-Invalid state applied to the native input and root data attribute.

Other supported div attributes are forwarded to the outer wrapper. children and a custom onDrop are not part of the API.

FileUploadUploadContext

FileUploadUploadHandler is (file: File, context: FileUploadUploadContext) => Promise<void>.

PropertyTypeDescription
onProgress(progress: number) => voidReports percentage progress; finite values are clamped to 0-100.
signalAbortSignalSignals cancellation on removal, replacement, retry, or unmount.

FileUploadRejection

PropertyTypeDescription
fileFileOriginal rejected file.
codeFileUploadRejectionCodeMachine-readable rejection reason.
messagestringUser-facing explanation.
Rejection codeMeaning
"file-type"MIME type does not match accept.
"file-too-large"File exceeds maxFileSize.
"total-too-large"Adding the file would exceed maxTotalSize.
"too-many-files"Selection exceeds maxFiles, or includes extra files in single mode.

Exported item types

FileUploadItem describes the internal list entries. It is exported for typing, but callbacks receive files or rejections, not item records.

PropertyTypeDescription
idstringGenerated item identity.
fileFileSelected file.
statusFileUploadStatus"queued", "uploading", "complete", or "error".
progressnumberPercentage progress.
errorstringOptional upload failure message.

Accessibility

The dropzone is keyboard focusable and uses button semantics. Enter and Space open the native picker; disabled dropzones are skipped by Tab. Removal and retry controls have filename-specific accessible names.

The selected-file list announces additions and removals politely. Rejection messages use an alert, upload errors and completion use status messages, and progress bars have file-specific labels. Drag feedback is supplementary: users can always choose files with the keyboard instead.

;