Quick Start Examples API Reference ✦ Live Demo GitHub npm
v0.2.0 — Now on npm

File uploads,
finally done right.

A modern, accessible, TypeScript-first React library for file uploads. Drag-and-drop, image previews, progress tracking — with zero backend opinions.

npm install react-dropkit
App.tsx
import { Dropzone } from 'react-dropkit' export default function App() { return ( <Dropzone multiple autoUpload={false} showUploadButton showClearButton uploadButtonLabel="Send files" validation={{ accept: ['image/*', '.pdf'], maxSize: 5 * 1024 * 1024, maxFiles: 5, }} onUpload={async (file) => { const fd = new FormData() fd.append('file', file.file) await fetch('/api/upload', { method: 'POST', body: fd }) }} /> ) }
~24KB
Bundle (ESM)
45
Tests passing
0
Dependencies
100%
TypeScript
Why react-dropkit

Everything you need.
Nothing you don't.

Built to fill the real gaps in existing file upload libraries — without the bloat.

🎯

TypeScript-first

Full types out of the box. No @types/ package needed. Every prop and hook return is fully typed.

Accessible by default

ARIA roles, keyboard navigation, screen reader support, and focus management — baked in from day one.

ESM + CJS dual build

Works in Next.js App Router, Vite, Remix, and Create React App. Fixes the broken ESM found in react-dropzone.

🖼️

Built-in previews

Images get real thumbnails from the file itself. Other types show clean icons with extension badges. No upload required.

📊

Per-file progress

Each file gets its own animated progress bar with live status updates. Ready → Uploading → Uploaded or Failed.

🧩

Headless or styled

Use <Dropzone /> for instant UI, or useFileUpload() to build a completely custom experience.

🔒

Smart validation

Accepts MIME wildcards, exact types, and extensions. Includes the cross-browser Windows/Safari MIME bug fix.

🔌

Zero backend opinions

Plug in your own upload logic — S3, Cloudinary, Supabase, your own API. You handle transport, we handle UI.

🌲

Tree-shakeable

Import only what you use. If you only import useFileUpload, only that ships to your users.

Quick Start

Up and running in 60 seconds.

1
Install the package
Works with React 17 and above.
npm install react-dropkit
2
Drop in the component
Import and use — no configuration needed to get started.
App.tsx
import { Dropzone } from 'react-dropkit'

export default function App() {
  return (
    <Dropzone
      validation={{ accept: ['image/*', '.pdf'], maxSize: 5 * 1024 * 1024 }}
      onFilesAdded={(files) => console.log(files)}
    />
  )
}
3
Add your upload handler
Connect any backend — the library manages UI state around your function.
App.tsx
<Dropzone
  onUpload={async (file) => {
    const formData = new FormData()
    formData.append('file', file.file)
    await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    })
  }}
/>
Examples

Every use case, covered.

BasicUpload.tsx
import { Dropzone } from 'react-dropkit'

export function BasicUpload() {
  return (
    <Dropzone
      multiple
      validation={{
        accept: ['image/*', '.pdf'],
        maxSize: 10 * 1024 * 1024,
        maxFiles: 5,
      }}
      onFilesChange={(files) => console.log(files.length)}
      onError={(err) => console.error(err)}
    />
  )
}
ManualUpload.tsx
// Files sit idle until the user clicks Upload.
// Use autoUpload=false + showUploadButton together.
<Dropzone
  autoUpload={false}
  showUploadButton
  showClearButton
  uploadButtonLabel="Send files"
  clearButtonLabel="Remove all"
  validation={{ accept: ['image/*', '.pdf'], maxFiles: 5 }}
  onUpload={async (file) => {
    const fd = new FormData()
    fd.append('file', file.file)
    await fetch('/api/upload', { method: 'POST', body: fd })
  }}
  onError={(err) => console.error(err)}
/>

// Custom styling example
<Dropzone
  className="my-wrapper"
  dropzoneClassName="my-drop-area"
  fileListClassName="my-file-list"
/>
CustomUpload.tsx
import { useFileUpload, FileList } from 'react-dropkit'

export function CustomUpload() {
  const {
    files, isDragActive, isDragReject,
    getRootProps, getInputProps, inputRef,
    removeFile, uploadAll, clearFiles,
  } = useFileUpload({
    multiple: true,
    validation: { accept: ['image/*'] },
    onUpload: async (file) => { /* your logic */ },
  })

  return (
    <div>
      <div {...getRootProps()}
        style={{ border: `2px dashed ${isDragReject ? 'red' : 'gray'}` }}
      >
        <input {...getInputProps()} ref={inputRef} />
        <p>{isDragActive ? 'Drop here!' : 'Click or drag'}</p>
      </div>
      <FileList files={files} onRemove={removeFile} />
      <button onClick={uploadAll}>Upload All</button>
    </div>
  )
}
ProfileForm.tsx
import { useState } from 'react'
import { useFileUpload } from 'react-dropkit'

export function ProfileForm() {
  const [name, setName] = useState('')
  const { files, getRootProps, getInputProps, inputRef } = useFileUpload({
    multiple: false,
    validation: { accept: ['image/*'] },
  })

  const handleSubmit = async (e) => {
    e.preventDefault()
    const fd = new FormData()
    fd.append('name', name)
    if (files[0]) fd.append('avatar', files[0].file)
    await fetch('/api/profile', { method: 'POST', body: fd })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <div {...getRootProps()}>
        <input {...getInputProps()} ref={inputRef} />
        <p>Drop avatar here</p>
      </div>
      <button type="submit">Save Profile</button>
    </form>
  )
}
AvatarUpload.tsx
import { useFileUpload } from 'react-dropkit'

export function AvatarUpload() {
  const { files, getRootProps, getInputProps, inputRef } = useFileUpload({
    multiple: false,
    validation: { accept: ['image/*'], maxSize: 2 * 1024 * 1024 },
  })

  return (
    <div {...getRootProps()}
      style={{ width: 96, height: 96, borderRadius: '50%', cursor: 'pointer' }}
    >
      <input {...getInputProps()} ref={inputRef} />
      {files[0]?.preview
        ? <img src={files[0].preview} style={{ borderRadius: '50%' }} />
        : <span>Upload photo</span>
      }
    </div>
  )
}
ProgressUpload.tsx
<Dropzone
  onUpload={(file) => new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    const fd = new FormData()
    fd.append('file', file.file)

    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        // real progress from the server
        const pct = Math.round((e.loaded / e.total) * 100)
      }
    }

    xhr.onload = () =>
      xhr.status === 200 ? resolve() : reject(new Error('Failed'))
    xhr.onerror = () => reject(new Error('Network error'))
    xhr.open('POST', '/api/upload')
    xhr.send(fd)
  })}
/>
API Reference

Every prop, documented.

<Dropzone /> props
PropTypeDescription
validationValidationOptionsFile type, size, and count rules.
multiplebooleanAllow multiple files. Default: true.
disabledbooleanDisables the drop zone entirely. Default: false.
autoUploadbooleanUpload files immediately on add. Set to false for manual upload. Default: true.
showUploadButtonbooleanRenders a built-in Upload All button below the file list. Default: false.
showClearButtonbooleanRenders a built-in Clear All button below the file list. Default: false.
uploadButtonLabelstringCustom label for the upload button. Default: "Upload All".
clearButtonLabelstringCustom label for the clear button. Default: "Clear All".
onFilesAdded(files: UploadedFile[]) => voidFires when new files pass validation.
onFilesChange(files: UploadedFile[]) => voidFires on every change. Always reflects the current files array.
onFileRemoved(id: string) => voidFires when a file is removed.
onUpload(file: UploadedFile) => Promise<void>Your upload handler. Library manages status around your Promise.
onError(error: string) => voidFires on validation failure or upload error.
classNamestringCustom class on the outer wrapper.
dropzoneClassNamestringCustom class on the inner drop zone area.
fileListClassNamestringCustom class on the file list.
childrenReactNodeOverride the default UI while keeping all logic.
ValidationOptions
OptionTypeDescription
acceptstring[]Wildcards (image/*), exact MIME types (image/png), extensions (.pdf). Mix freely.
maxSizenumberMax file size in bytes. e.g. 5 * 1024 * 1024 for 5MB.
minSizenumberMin file size in bytes.
maxFilesnumberMax total files allowed.
useFileUpload() returns
ValueTypeDescription
filesUploadedFile[]Current files with status, progress, preview.
isDragActivebooleanTrue when dragging over the drop zone.
isDragRejectbooleanTrue when dragged files don't match accepted types.
getRootProps()HTMLAttributesSpread onto your container. Handles drag events, click, keyboard.
getInputProps()InputHTMLAttributesSpread onto a hidden input for file browser support.
inputRefRefObject<HTMLInputElement>Attach to input so clicking opens the file browser.
addFiles(files)(File[]) => voidProgrammatically add files.
removeFile(id)(string) => voidRemove a file by ID.
clearFiles()() => voidRemove all files and revoke preview URLs.
uploadAll()() => Promise<void>Upload all idle or failed files in parallel.
openFileDialog()() => voidProgrammatically open the native file browser.
Comparison

How it stacks up.

Against the libraries developers reach for today.

Featurereact-dropzonereact-dropkit ✦FilePond
Built-in UI
Image previews✕ Manual✓ Auto
Progress bars✕ Manual✓ Built-in
Headless hook
TypeScript-first⚠ @types/ needed✓ Native⚠ Afterthought
ESM + Next.js⚠ Broken ESM✓ Full ESM + CJS
Zero dependencies
Accessibility⚠ Partial✓ WCAG⚠ Partial