mwui
GitHub

File Dropzone

A drag-and-drop file input with previews, validation, and per-file progress.

Drop attachments here

IMAGES, PDF · up to 2 MB · 4 files max

Installation

Usage

A drop target, a real file input, image thumbnails, size and type validation, and a progress row per file. All of the state lives in useFileUpload, so the component is markup — swap it for your own layout without rewriting the logic.

import { FileDropzone } from "@/components/ui/file-dropzone";

export function Attachments() {
  return (
    <FileDropzone
      accept="image/*,.pdf"
      maxSize={5 * 1024 * 1024}
      maxFiles={5}
      onFilesChange={setAttachments}
    />
  );
}

Without onUpload the dropzone is a collector: it validates and lists files and hands them to onFilesChange, and you submit them with the rest of your form.

Uploading

Pass onUpload and each accepted file uploads immediately, with a progress bar, an error state, and a retry button.

<FileDropzone
  accept="image/*"
  onUpload={async (item, { onProgress, signal }) => {
    const body = new FormData();
    body.append("file", item.file);

    await fetch("/api/upload", { method: "POST", body, signal });
    onProgress(1);
  }}
/>

signal aborts if the file is removed mid-flight, so cancelling actually cancels. Real byte-level progress needs XMLHttpRequestfetch cannot report it — and onProgress takes a fraction from 0 to 1.

Validation

accept is enforced on drop as well as in the picker. The browser applies the accept attribute only to the file dialog, so a dragged .exe would otherwise sail through.

Rejected files never enter the list; they render above it with a specific reason — wrong type, too large, over the file count, or already added.

Props

PropDescription
acceptSame syntax as the accept attribute: "image/*,.pdf".
maxSizePer-file limit in bytes.
maxFilesMaximum number of accepted files.
multipleWhen false, a new selection replaces the current file.
onUploadUpload transport. Enables progress, errors, and retry.
onFilesChangeCalled with the full list after every change.
labelHeadline inside the drop target.
descriptionOverrides the hint line, which is otherwise derived from the constraints.
disabledIgnore selection and drops.

Behavior

  • Drag state uses enter/leave depth counting, so dragging across a child element does not flicker the highlight.
  • Image previews are object URLs, revoked when the file is removed or the component unmounts.
  • The file input stays in the DOM and is visually hidden rather than replaced by a button, so keyboard and assistive-technology users get the native picker.