Skip to content
Forms

Dropzone — Component Library Guide

Simple HTML5-compliant drag-and-drop zone for files. Hooks-based API, MIME type filtering, file size limits, and multiple file support.

TypeScript MIT
2.4M weekly downloads
10.8k GitHub stars
~11kB bundle size
v14.3.5 latest version
file-uploaddrag-and-drophooksvalidationhtml5

TL;DR: React Dropzone wraps HTML5 File API drag events into a clean useDropzone() hook. Handles click-to-browse, MIME type filtering, file size limits, and multi-file support. Pair it with React Hook Form's <Controller> for validated file uploads. Official docs: react-dropzone.js.org.

What is React Dropzone?

React Dropzone provides a useDropzone hook and an <Dropzone> component for file drag-and-drop upload zones. It abstracts the HTML5 File API drag events, handles click-to-browse, validates MIME types and file sizes, and provides getRootProps / getInputProps for rendering your own UI.

When to use it

Use React Dropzone when users need to upload files - images, documents, CSVs. It integrates with React Hook Form via Controller and pairs well with direct-to-S3 uploads or presigned URL patterns.

Why use it instead of a raw file input?

You can absolutely build an upload zone with a plain <input type="file"> and a few drag event listeners. People do it, and then they discover the long tail of edge cases. Drag events fire on child elements and bubble in confusing ways. The native input is famously hard to style. Detecting whether the user is dragging a valid file versus dragging text from another tab takes real effort. React Dropzone has already solved all of that and ships it in roughly 11kB.

What you actually get is a headless hook. It doesn't render a single styled element on its own. You spread getRootProps() on your container and getInputProps() on a hidden input, and the library wires up the events, the keyboard handling, and the click-to-browse fallback. Everything visible is your markup, so it fits whatever design system you already use. That's why the same library shows up in apps that look nothing alike.

Validation and the rejection flow

The part people underuse is the rejection handling. When a file fails a check, it doesn't silently disappear. It lands in fileRejections, an array where each entry carries the file and an errors list with stable codes like file-too-large, file-invalid-type, and too-many-files. Those codes are the reliable thing to branch on, not the human-readable messages, which can change.

A pattern I reach for on every upload form: render accepted files in one list and rejected files in another, each rejected item showing exactly why it failed. Users hate a dropzone that just refuses their file with no explanation. Surfacing "that PDF is 12MB, the limit is 5MB" turns a frustrating dead end into something they can fix in one try.

const { fileRejections } = useDropzone({ maxSize: 5 * 1024 * 1024 })

{fileRejections.map(({ file, errors }) => (
  <p key={file.name} className="text-red-600 text-sm">
    {file.name}: {errors.map(e => e.message).join(', ')}
  </p>
))}

Keep in mind that accept filtering happens at the browser level for the file picker, but the drop validation is what truly enforces your rules. A determined user can drag in anything, so always validate again on the server. Client-side checks are for fast feedback, not security.

Accessibility and memory notes

The hook handles keyboard access for you. The root element becomes focusable and responds to Enter and Space to open the picker, which means keyboard-only users aren't locked out. If you build your own zone from scratch, this is the bit most people forget. Do add a visible focus style and a real <label> or aria-label describing the action, since "drag and drop here" doesn't translate to assistive tech on its own.

The memory leak trap is worth repeating because it bites everyone once. When you generate image previews with URL.createObjectURL(file), each call allocates a blob URL that the browser holds until you free it. Store the URLs in state, and in a cleanup effect call URL.revokeObjectURL(url) for each one when the component unmounts or the file list changes. Skip this and a heavy upload page slowly eats memory across a session, which is the kind of bug that never shows up in a quick demo but does in production.

Key Features

  • useDropzone hook - headless; you control the rendering
  • MIME type filtering - accept prop with MIME type map
  • File size limits - maxSize, minSize in bytes
  • Multiple files - multiple prop; maxFiles limit
  • Drag-active state - isDragActive, isDragReject for visual feedback
  • Programmatic open - open() method to trigger file picker without a click

Installation

npm install react-dropzone

Basic Usage

import { useDropzone } from 'react-dropzone'
import { useCallback } from 'react'

function FileUpload() {
  const onDrop = useCallback((acceptedFiles: File[]) => {
    // Do something with the files
    acceptedFiles.forEach(file => console.log(file.name, file.size))
  }, [])

  const { getRootProps, getInputProps, isDragActive, isDragReject, acceptedFiles } = useDropzone({
    onDrop,
    accept: { 'image/*': ['.png', '.jpg', '.webp'], 'application/pdf': ['.pdf'] },
    maxSize: 5 * 1024 * 1024,  // 5MB
    multiple: true,
    maxFiles: 5,
  })

  return (
    <div
      {...getRootProps()}
      className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-colors ${
        isDragReject ? 'border-red-400 bg-red-50'
        : isDragActive ? 'border-indigo-400 bg-indigo-50'
        : 'border-slate-300 hover:border-indigo-400'
      }`}
    >
      <input {...getInputProps()} />
      <p className="text-slate-500">
        {isDragReject ? 'File type not accepted'
          : isDragActive ? 'Drop files here...'
          : 'Drag files here, or click to browse'}
      </p>
      {acceptedFiles.length > 0 && (
        <ul className="mt-4 text-left text-sm">
          {acceptedFiles.map(f => <li key={f.name}>{f.name} ({(f.size / 1024).toFixed(1)}kB)</li>)}
        </ul>
      )}
    </div>
  )
}

Integration with React Hook Form

import { Controller } from 'react-hook-form'
import { useDropzone } from 'react-dropzone'

function FileField({ name, control }: { name: string; control: Control }) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field }) => {
        const { getRootProps, getInputProps } = useDropzone({
          onDrop: (files) => field.onChange(files),
          multiple: false,
        })
        return (
          <div {...getRootProps()} className="border-2 border-dashed rounded-lg p-6 text-center cursor-pointer">
            <input {...getInputProps()} />
            {field.value?.[0]?.name ?? 'Drop a file or click to select'}
          </div>
        )
      }}
    />
  )
}

TypeScript Tips

Type accepted files explicitly:

const { acceptedFiles, fileRejections } = useDropzone({
  accept: { 'text/csv': ['.csv'] },
})

// fileRejections is typed as FileRejection[]
fileRejections.forEach(({ file, errors }) => {
  errors.forEach(e => console.error(e.code, e.message))
})

Common Gotchas

  • accept changed in v12 - the old string format ('image/*') was replaced by a MIME type object ({ 'image/*': ['.jpg'] }). The new format is more explicit about allowed extensions
  • onDrop fires for rejected files too - use onDropAccepted and onDropRejected separately if you need to handle them differently
  • Preview URLs - generate with URL.createObjectURL(file) and revoke with URL.revokeObjectURL() in a cleanup effect to avoid memory leaks
  • React Hook Form - wrap React Dropzone with <Controller> to add file upload to validated forms
  • React Select - another complex input that pairs with React Hook Form via <Controller>

Frequently Asked Questions

How do I restrict accepted file types in React Dropzone?

Use the accept prop with a MIME type object. For example: accept={{ 'image/*': ['.jpg', '.png', '.webp'], 'application/pdf': ['.pdf'] }}. The keys are MIME types and the values are allowed extensions. This format changed in v12 - the old string format is no longer supported.

How do I limit file size and number of files?

Pass maxSize (in bytes) and maxFiles to the useDropzone hook. For example: maxSize={5 * 1024 * 1024} limits to 5MB, and maxFiles={3} allows up to 3 files. Files that exceed limits appear in fileRejections with an error code.

How do I integrate React Dropzone with React Hook Form?

Wrap useDropzone inside a Controller component. In the onDrop callback, call field.onChange(files) to pass the File array to the form. Access the files with watch('fieldName') and display validation errors from formState.errors.

How do I show image previews after dropping files?

Generate preview URLs with URL.createObjectURL(file) inside your onDrop callback and store them in state. Display them as <img src={preview} />. Important: clean up with URL.revokeObjectURL(url) in a useEffect cleanup to prevent memory leaks.

What is the difference between onDrop, onDropAccepted, and onDropRejected?

onDrop fires for all dropped files, including rejected ones. onDropAccepted fires only for files that passed validation. onDropRejected fires only for files that failed validation, receiving an array of FileRejection objects with the file and error details. Use the specific callbacks when you need to handle each case separately.