Dropzone
<Dropzone> is the file-ingestion area — drag-and-drop or click to browse. Wraps react-dropzone. Pair with <DropzoneItem> rows to show staged files.
When to use
- File uploads in agent ingestion, document import, attachment fields.
- Bulk imports (CSV / JSON / images).
- Anywhere a
<input type="file">would otherwise live alone.
Don’t use Dropzone for: paste-only flows (clipboard images), drag-to-reorder list items (different gesture, different primitive), or a single-file picker that already fits in a row (a Button with <input type="file"> hidden inside is fine).
Default
Drop files or click to browse. The consumer owns the staged-files list.
Drop files here, or click to browse
Any file type · 10MB max each
const [files, setFiles] = useState<File[]>([]);
<Dropzone multiple label="Drop files here, or click to browse" hint="Any file type · 10MB max each" onFilesAccepted={(accepted) => setFiles((p) => [...p, ...accepted])}> {files.map((f, i) => ( <DropzoneItem key={`${f.name}-${i}`} filename={f.name} size={f.size} status="success" onRemove={() => setFiles((p) => p.filter((_, idx) => idx !== i))} /> ))}</Dropzone>Staged files with progress and errors
<DropzoneItem> is presentation-only — the consumer drives status / progress / errors.
Drop files here, or click to browse
report-2026-q3.pdf1.4 MB
invoice-april.csv82.0 KB
too-big.zip120.0 MB
File exceeds 100MB limit.
<Dropzone> <DropzoneItem filename="report.pdf" size={1.4 * 1024 * 1024} status="uploading" progress={42} /> <DropzoneItem filename="invoice.csv" size={84_000} status="success" onRemove={remove} /> <DropzoneItem filename="too-big.zip" size={120 * 1024 * 1024} status="error" error="File exceeds 100MB limit." onRemove={remove} /></Dropzone>API
Dropzone
| Prop | Type | Default | Description |
|---|---|---|---|
onFilesAccepted | (files: File[]) => void | — | Files that passed all the accept/size/count rules. |
onFilesRejected | (rejections: FileRejection[]) => void | — | Files that failed validation. Each rejection carries the reason. |
onDrop | (accepted, rejections) => void | — | Combined handler — fires regardless. Use this when you need both arrays at once. |
multiple | boolean | false | Allow multiple files in one drop / pick. |
accept | Accept | — | MIME-type → extensions map. e.g., { "application/pdf": [".pdf"] }. |
maxSize | number | — | Max bytes per file. |
maxFiles | number | — | Cap when multiple is true. |
label | ReactNode | "Drop files here, or click to browse" | Replace the default affordance text. |
hint | ReactNode | — | Optional secondary line — typically the file-type / size constraints. |
size | "default" | "compact" | "default" | Vertical density. |
children | ReactNode | — | Staged-file list — typically <DropzoneItem>s. |
DropzoneItem
| Prop | Type | Default | Description |
|---|---|---|---|
filename * | string | — | Visible filename. |
size | number | — | Bytes — formatted as KB / MB / GB. |
status | "queued" | "uploading" | "success" | "error" | "queued" | Drives the visual state. |
progress | number | — | Percent 0–100. Renders the inline track when status="uploading". |
error | ReactNode | — | Inline error message. Renders only when status="error". |
onRemove | () => void | — | Callback for the trailing X. The X is hidden when omitted. |
Design guidelines
✓ Do
- Always set accept and maxSize. Without them, users can drop a 4GB ISO on a "PDF only" form.
- Show staged files inline as DropzoneItems — even after upload completes — so the user can verify and remove.
- Surface rejection reasons. Silent rejection looks like the dropzone is broken.
✗ Don't
- Hide the click-to-browse fallback. Drag is hostile on touch and accessibility-unfriendly without keyboard.
- Auto-upload without showing progress. The user assumes nothing's happening.
- Use Dropzone for clipboard-only paste flows. Different primitive (still on the roadmap).
Accessibility
- Root has
role="button"andtabIndex={0}— keyboard-activatable. Space / Enter opens the file picker. - Each
DropzoneItemremove button isaria-label="Remove <filename>". - Drag-state visuals (active / accept / reject) are paired with explicit text labels — color is never the only signal.