GitHub

Dropzone

Build custom file intake surfaces with a headless drag/drop and file input primitive.

Dropzone is the headless file-intake primitive. It owns drag/drop mechanics, file input wiring, accept validation, max-size and max-file rejection, controlled/uncontrolled file state, and accessibility prop getters.

Browser file intake is not upload. Dropzone decides which local files enter React state; your application decides whether, when, and how those files are uploaded.

It does not render icons, thumbnails, copy, grids, cards, progress rows, or upload transport.

Installation

pnpm dlx shadcn@latest add @retab/dropzone

Example

Drop or browse files
Headless useDropzone controls this custom surface.
No files selected.

Mental Model

A dropzone is not a rectangle. It is the browser file-intake behavior normalized into React state and prop getters.

  • files is selected-file state.
  • lastIntake is the latest file-intake attempt.
  • onFilesChange reports selected-file state changes.
  • onIntake reports accepted and rejected files from one attempt.
  • getTriggerProps makes any element open the file dialog; pass native when that element is a real <button>.

Trigger Patterns

getTriggerProps is the one opener. By default it polyfills button semantics onto any element — role, focusability, and Enter/Space activation. Pass native: true when the trigger is a real <button> so the platform owns those and the getter only supplies the disabled attribute and type="button".

Non-button trigger
Controls upload
No files attached
Native button trigger
A real button uses browser button semantics.
Controlled queue
Parent-owned file state.
Validation only
PDF under 100 KB. Accepted files are reported, not stored.
Accepted: none
Disabled state
The primitive disables input, trigger focus, and drag state.

File Compositions

Dropzone stays headless even when the visual surface is rich. These examples compose it with FileThumbnail and FileViewer for selected file previews and document intake flows.

Custom thumbnail grid
Direct useDropzone composition with FileThumbnail.
Drop PDFs or images here.
File preview
Upload file
Drop a file here to open it in the viewer.
Avatar image slot
One image, replaceable by design.
Drop profile image
PNG, JPG, or WebP.
Spreadsheet mapper
A single sheet feeds a mapping workflow.
Drop CSV or XLSX to preview columns.
Audio transcript queue
Audio or video files become transcript jobs.
Drop interview recordings here.

Loading Bar

Intake is not upload. Once Dropzone admits a file into React state, your application owns the transport. This example keys per-file upload progress off the dropzone's file id and renders a progress bar; swap the simulated interval for fetch or XMLHttpRequest with an onprogress handler in real code.

Upload queue
Intake into React state, then upload.
Drop files to start uploading.

The file-intake viewer keeps browser acquisition separate from viewer chrome:

<FileIntakeViewerProvider>
  <FileIntakeViewerDropTarget>
    <FileIntakeViewerRoot>
      <FileIntakeViewerHeader />
      <ViewerBody>
        <FileIntakeViewerSidebar />
        <FileIntakeViewerSurface />
      </ViewerBody>
    </FileIntakeViewerRoot>
  </FileIntakeViewerDropTarget>
</FileIntakeViewerProvider>

FileIntakeViewerDropTarget owns the drop root and hidden file input. FileIntakeViewerRoot owns only the viewer frame.

Workflow Surfaces

Custom products usually need file intake inside a domain-specific state shape: comparison slots, required packet checklists, routed intake lanes, timelines, or pinboards. Dropzone owns intake; the surrounding component owns the workflow.

Evidence timeline
Files become ordered events inside a custom surface.
Drop PDFs or images to build a case timeline.
Comparison pair
Two independent dropzones model original versus revision.
2 slots
Original
Drop original document.
Revision
Drop revision document.
Intake router
One target, derived lanes by file type.
Documents0
No documents yet.
Images0
No images yet.
Tables0
No tables yet.
Required packet
Slot-level dropzones for checklist-driven uploads.
checklist
Identity proof
open
Drop required file.
Bank statement
open
Drop required file.
Board approval
open
Drop required file.
Pinboard drop surface
The whole canvas is the trigger.
Drop files to pin them onto the canvas.

Behavior

  • Uses the same commit path for drag/drop and native file input selection.
  • Opens the native file picker through openFileDialog or trigger props.
  • Tracks nested drag enter/leave events so active state does not flicker.
  • Highlights only when the drag payload contains files.
  • Accepts MIME types, wildcard MIME types such as image/*, and extensions.
  • Supports controlled and uncontrolled file state.
  • Supports multiple, maxFiles, and maxSize.
  • Exposes removeFile, clearFiles, resetIntake, and reset.
  • Reports each intake attempt through lastIntake and onIntake.
  • Reports rejection facts, not display copy.
  • Composes consumer event handlers and respects event.defaultPrevented.

API Reference

useDropzone

PropTypeDefaultRequired
acceptstring-No
defaultFilesDropzoneFileItem[][]No
disabledbooleanfalseNo
filesDropzoneFileItem[]-No
maxFilesnumber-No
maxSizenumber-No
multiplebooleantrueNo
onFilesChange(files: DropzoneFileItem[]) => void-No
onIntake(intake: DropzoneIntake) => void-No

Return Value

FieldType
filesDropzoneFileItem[]
lastIntakeDropzoneIntake
isDraggingboolean
isDisabledboolean
clearFiles() => void
resetIntake() => void
reset() => void
openFileDialog() => void
removeFile(fileId: string) => void
getRootProps(props?) => React.HTMLAttributes<HTMLElement>
getInputProps(props?) => React.InputHTMLAttributes<HTMLInputElement>
getTriggerProps(props? & { native?: boolean }) => props

The package also exports the public types DropzoneFileItem, DropzoneIntake, DropzoneFileRejection, UseDropzoneProps, and UseDropzoneReturn.

Validation helpers are exported for tests and advanced compositions: parseDropzoneAccept, matchesDropzoneAccept, validateDropzoneFile, and validateDropzoneFiles.

DropzoneFileRejection is a discriminated union:

ReasonExtra fields
file-invalid-typeacceptRules
file-too-largemaxSize
too-many-filesmaxFiles

The primitive does not export rejection messages or file-size formatting. Render copy in the visual layer.

Source

dropzone.tsx