npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-headless-dropzone

v1.0.0

Published

A production-ready, fully headless React dropzone & file-upload library. Behaviour, validation, previews, an upload queue and full accessibility — zero styling, zero runtime dependencies.

Readme

react-headless-dropzone

A production-ready, fully headless React dropzone & file-upload library.

Behaviour, validation, previews, an upload queue and complete accessibility — with zero styling and zero runtime dependencies.

npm bundle size types license


Why another dropzone?

react-headless-dropzone gives you both: a complete file-management engine (validation, deduplication, previews, an abortable concurrent upload queue, retries) exposed through a Radix-style headless API where you own 100% of the markup and styling.

Features

  • 🧲 Drag & drop, click-to-select, keyboard, touch and clipboard paste
  • 📁 Directory drops & folder selection (recursive, with relative paths)
  • ✅ Validation — type, size, min/max files, sync and async custom validators
  • 🖼️ Lazy previews for images, video, audio and PDFs with automatic object-URL cleanup
  • 🔁 Upload queue — concurrency limit, progress, cancel, retry, AbortSignal
  • 🧬 Duplicate detection, file replacement, single-file mode
  • ♿ Accessible by default — roles, ARIA, focus management, full keyboard support
  • 🎛️ Controlled & uncontrolled modes
  • 🧩 Two APIs — a powerful useDropzone hook and composable <Dropzone.*> components
  • 🎨 Truly headless — every element takes className/style (static or a function of state), asChild, render props, and rich data-* attributes
  • 🌳 Tree-shakeable, ESM + CJS, ships .d.ts + source maps, "use client" ready
  • ⚛️ React 18 & 19, Strict-Mode clean, SSR/RSC safe

Installation

npm install react-headless-dropzone
# or
pnpm add react-headless-dropzone
# or
yarn add react-headless-dropzone

react (>=18) is the only peer dependency.

Quick start

With the hook

import { useDropzone } from 'react-headless-dropzone';

function Uploader() {
  const { getRootProps, getInputProps, acceptedFiles, isDragActive } = useDropzone({
    accept: { 'image/*': ['.png', '.jpg', '.jpeg'] },
    maxSize: 5 * 1024 * 1024,
  });

  return (
    <div {...getRootProps()} className="dropzone">
      <input {...getInputProps()} />
      {isDragActive ? <p>Drop them here…</p> : <p>Click or drag files to upload</p>}
      <ul>
        {acceptedFiles.map((file) => (
          <li key={file.id}>{file.name}</li>
        ))}
      </ul>
    </div>
  );
}

With compound components

Copy-paste ready, styled with Tailwind. This is exactly the code that produces the screenshot below — nothing omitted:

import { Dropzone } from 'react-headless-dropzone';
import { uploadFile } from './upload'; // your own handler — see "Uploading files"

export function Uploader() {
  return (
    <Dropzone
      accept={{ 'image/*': [], 'application/pdf': ['.pdf'] }}
      maxFiles={5}
      autoUpload
      onUpload={uploadFile}
      className="relative rounded-xl border-2 border-dashed border-zinc-300 bg-white p-4 transition-colors data-drag-active:border-indigo-500 data-drag-active:bg-indigo-50 data-drag-reject:border-red-500 data-drag-reject:bg-red-50 data-focused:outline-2 data-focused:outline-offset-2 data-focused:outline-indigo-500"
    >
      <Dropzone.Input />

      <Dropzone.Empty className="cursor-pointer p-8 text-center text-sm text-zinc-500">
        <span className="font-medium text-zinc-900">Click to browse</span> or drag files here
      </Dropzone.Empty>

      <Dropzone.DragOverlay className="pointer-events-none absolute inset-0 grid place-items-center rounded-lg bg-indigo-500/10 font-semibold text-indigo-700">
        Drop to upload
      </Dropzone.DragOverlay>

      <Dropzone.FileList as="ul" className="grid gap-2">
        {(file) => (
          <li
            data-status={file.status}
            className="flex items-center gap-3 rounded-lg border border-zinc-200 bg-white p-2.5 data-[status=error]:border-red-300 data-[status=error]:bg-red-50 data-[status=rejected]:border-red-300 data-[status=rejected]:bg-red-50 data-[status=success]:border-green-300"
          >
            <Dropzone.Preview
              className="h-10 w-10 shrink-0 rounded-md object-cover"
              fallback={
                <span className="grid h-10 w-10 shrink-0 place-items-center rounded-md bg-zinc-100 text-[10px] font-bold uppercase text-zinc-400">
                  {file.name.split('.').pop()}
                </span>
              }
            />

            <div className="min-w-0 flex-1">
              <Dropzone.FileName className="block truncate text-sm font-medium text-zinc-900" />
              <Dropzone.FileSize className="text-xs text-zinc-500" />

              {/* FileProgress exposes --rhd-progress (e.g. "62%") for the fill */}
              <Dropzone.FileProgress className="mt-1.5 h-1 overflow-hidden rounded-full bg-zinc-100 after:block after:h-full after:w-(--rhd-progress) after:bg-indigo-500 after:transition-[width] data-[status=error]:after:bg-red-500 data-[status=success]:after:bg-green-500" />

              <Dropzone.FileErrors className="mt-1 list-none p-0 text-xs text-red-600" />
            </div>

            <Dropzone.RemoveButton
              aria-label={`Remove ${file.name}`}
              className="shrink-0 rounded-md px-2 py-1 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-900"
            >
              ✕
            </Dropzone.RemoveButton>
          </li>
        )}
      </Dropzone.FileList>
    </Dropzone>
  );
}

No CSS import needed — the classes above are plain Tailwind. Not using Tailwind? Every one of those data-[…] variants is just a real data-* attribute, so the same design works in vanilla CSS (.dropzone[data-drag-active] { … }) or any CSS-in-JS. See Styling and the showcase for ten more looks.

Demos

Ten designs, one component. Each of these is the same engine with different Tailwind classes — no forks, no variants, no theme config.

→ See all 10 examples with full source code ←

Also included: Minimal · Modern gradient · Landing hero · Avatar picker

Every screenshot is captured by a headless browser driving the real components (npm run showcase), so the previews can never drift from actual behaviour. Run them yourself with npm run dev → Styling showcase.

Uploading files

Provide an onUpload handler. It receives an AbortSignal (for cancellation) and an onProgress callback, and may resolve with any value or throw to fail.

const dz = useDropzone({
  autoUpload: true, // start as soon as files are accepted
  maxConcurrentUploads: 3,
  onUpload: async ({ file, signal, onProgress }) => {
    const body = new FormData();
    body.append('file', file.file);

    const res = await fetch('/api/upload', { method: 'POST', body, signal });
    if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
    onProgress(100);
    return res.json();
  },
});

// Manual control:
dz.uploadAll();
dz.uploadFile(id);
dz.cancelUpload(id);
dz.retryUpload(id);

Real progress with XMLHttpRequest:

onUpload: ({ file, signal, onProgress }) =>
  new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/api/upload');
    xhr.upload.onprogress = (e) => e.lengthComputable && onProgress((e.loaded / e.total) * 100);
    xhr.onload = () =>
      xhr.status < 400 ? resolve(xhr.response) : reject(new Error(xhr.statusText));
    xhr.onerror = () => reject(new Error('Network error'));
    signal.addEventListener('abort', () => xhr.abort());
    const body = new FormData();
    body.append('file', file.file);
    xhr.send(body);
  });

Styling

The library renders no styles of its own. Style it through any of these mechanisms, in whatever combination suits your stack (Tailwind, CSS Modules, Emotion, styled-components, vanilla CSS, …):

1. data-* state attributes on the root and every slot:

| Attribute | On | Meaning | | --------------------------------------- | ------------------------ | -------------------------------------------------------------------------- | | data-dropzone | root | marker | | data-drag-active | root | a drag is over the zone | | data-drag-accept / data-drag-reject | root | the drag would be (dis)allowed | | data-focused | root | root has focus | | data-disabled | root, buttons | disabled | | data-uploading | root | an upload is in flight | | data-status | file slots | ready | rejected | uploading | success | error | canceled | | data-progress | FileProgress | 0–100 | | --rhd-progress | FileProgress (CSS var) | e.g. 73% |

.dropzone[data-drag-active] {
  border-color: dodgerblue;
}
[data-dropzone-file-progress]::after {
  width: var(--rhd-progress);
}

2. className / style as functions of state:

<Dropzone className={(s) => (s.isDragReject ? 'ring-red-500' : 'ring-gray-200')} />

3. asChild to render your element (Radix Slot pattern):

<Dropzone.Trigger asChild>
  <MyButton variant="primary">Browse…</MyButton>
</Dropzone.Trigger>

4. Render props — every slot's children can be a function of state:

<Dropzone.FileStatus>
  {({ file }) => (file.status === 'success' ? '✓ Uploaded' : file.status)}
</Dropzone.FileStatus>

5. Go fully custom — ignore the components entirely and build your own UI on top of useDropzone. The hook exposes everything.

Two ways to build

| | Hook (useDropzone) | Components (<Dropzone.*>) | | ----------- | ------------------------------- | ----------------------------- | | Control | Maximum — you render every node | High — compose provided slots | | Boilerplate | More | Less | | Best for | Bespoke designs, design systems | Getting productive fast |

Both share the exact same engine; you can even mix them via <Dropzone dropzone={api}>.

Documentation

Accessibility

The root is a focusable role="button"; Enter/Space open the file dialog, the file <input> is visually hidden but reachable by assistive tech, drag state is exposed for screen-reader-friendly messaging, and every provided button is a real, labellable <button>. Bring your own aria-label and aria-describedby (both are forwarded).

SSR / React Server Components

The package ships with the "use client" directive and guards every browser API (window, URL.createObjectURL, document) so it renders safely on the server and hydrates without mismatches. Import it inside a Client Component.

Browser support

All evergreen browsers. Directory drops use the widely-supported webkitGetAsEntry API and degrade gracefully to flat file lists elsewhere.

Contributing

Contributions are welcome! See CONTRIBUTING.md. Run the dev playground with npm run dev.

License

MIT © Zammad Nasir