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 XMLHttpRequest — fetch 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
| Prop | Description |
|---|---|
accept | Same syntax as the accept attribute: "image/*,.pdf". |
maxSize | Per-file limit in bytes. |
maxFiles | Maximum number of accepted files. |
multiple | When false, a new selection replaces the current file. |
onUpload | Upload transport. Enables progress, errors, and retry. |
onFilesChange | Called with the full list after every change. |
label | Headline inside the drop target. |
description | Overrides the hint line, which is otherwise derived from the constraints. |
disabled | Ignore 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.