Skip to content

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.

import from "@na/ui/components/Dropzone" ▶ Open in Storybook packages/ui/src/components/Dropzone.tsx

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" and tabIndex={0} — keyboard-activatable. Space / Enter opens the file picker.
  • Each DropzoneItem remove button is aria-label="Remove <filename>".
  • Drag-state visuals (active / accept / reject) are paired with explicit text labels — color is never the only signal.
  • Card — Wrap the Dropzone in a Card for form-page layouts.
  • Form — Spread the field onto Dropzone via Controller for react-hook-form integration.

▶ Open Dropzone stories in Storybook