September 16, 2026

React dropzone patterns for video uploads

Santhosh Lingabalan
Santhosh Lingabalan
Content & Brand Marketer - GTM

Drop a file onto a React drop zone that has any child elements, and the highlight flickers. The zone lights up, goes dark, lights up again as the pointer crosses a label or an icon. The cause is that dragleave fires when the pointer enters a child node, not only when it leaves the zone. A boolean can't represent that, and almost every hand-rolled drop zone starts with a boolean.

TL;DR

A React drop zone needs four events and a counter. dragenter and dragleave fire per element including children, so a boolean flag flickers as the pointer crosses a label; increment and decrement a depth counter instead. dragover must call preventDefault() on every tick or the browser navigates away to the file. Validate by type and size at the drop, not after the transfer. Rejecting a two-gigabyte file once it has uploaded is the worst possible order. Then hand the accepted file to an uploader that chunks it.

Why a drop zone is harder than one event handler

Four events do the work, and they don't behave the way their names suggest. dragenter and dragleave fire per element, including children. dragover fires continuously, and its default action is to reject the drop. Miss preventDefault() on it and the browser opens the file in a new tab instead. Only drop gives you the payload.

On top of that, a drop isn't the same event as a file picker selection. The picker gives you a FileList. A drop gives you a DataTransfer, which may contain files, directories, text, or a URL dragged from another tab.

EventFires on childrenYou must call preventDefaultWhat you get
dragenterYesYesNothing useful
dragoverContinuouslyYes, or the drop is rejectedNothing useful
dragleaveYes, on every child boundaryNoNothing useful
dropNoYesevent.dataTransfer

What you need before you start

  • A React 18 or later app. The examples are plain React with no drag and drop library.
  • A backend you control, if uploads are signed. The drop zone is client-side, the credential isn't.
  • A FastPix account with an Access Token ID and Secret Key from activate your account, if you want the upload half to work end to end.

Step 1: Count enters and leaves instead of toggling a boolean

The fix for the flicker is a depth counter. Increment on dragenter. Decrement on dragleave. Treat the zone as active only while the count is above zero:

text
import { useRef, useState } from "react";

export default function DropZone({ onFile }) {
  const depth = useRef(0);
  const [active, setActive] = useState(false);

  function onDragEnter(event) {
    event.preventDefault();
    depth.current += 1;
    setActive(true);
  }

  function onDragLeave(event) {
    event.preventDefault();
    depth.current -= 1;
    if (depth.current === 0) setActive(false);
  }

  function onDrop(event) {
    event.preventDefault();
    depth.current = 0;
    setActive(false);
    const file = event.dataTransfer.files?.[0];
    if (file) onFile(file);
  }

  return (
    <div
      onDragEnter={onDragEnter}
      onDragOver={(e) => e.preventDefault()}
      onDragLeave={onDragLeave}
      onDrop={onDrop}
      data-active={active || undefined}
    >
      Drag a video here
    </div>
  );
}

Keep the counter in a ref rather than state. State updates are batched and asynchronous, so a counter held in state reads a stale value during a fast drag across several children.

Step 2: Read what was actually dropped

dataTransfer.files is the quick path and it covers most cases. It also silently ignores directories, which is how a user drags a folder of footage onto your zone and nothing happens at all.

dataTransfer.items is the honest version. Each item carries a kind of either "file" or "string". Calling webkitGetAsEntry() tells you whether the entry is a file or a directory:

text
function readDrop(event) {
  const files = [];
  const folders = [];

  for (const item of event.dataTransfer.items) {
    if (item.kind !== "file") continue;
    const entry = item.webkitGetAsEntry?.();
    if (entry?.isDirectory) folders.push(entry.name);
    else files.push(item.getAsFile());
  }

  return { files, folders };
}

You can run this against a real upload before committing to it. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.

Telling the user "folders can't be uploaded, drop the files inside instead" is a two-line change and removes a support ticket category. Recursing into the directory is possible with createReader(), but for video it's usually the wrong product decision anyway.

Step 3: Validate before a single byte moves

Three checks belong before the upload starts, because each one is cheap now and expensive later.

Type is the obvious one: compare against your accept list rather than trusting the extension. Size is the next, and it matters more for video than for anything else. A rejection after twenty minutes of uploading is a genuinely bad experience. The third check is the one most drop zones skip: whether the browser will actually let you read the bytes.

Android makes that last case real. accept="video/*" sometimes opens the Photos or Gallery app instead of the file manager. The browser can then hand your page a File object whose contents it can't read. The upload then starts, transfers nothing, and fails with an error that describes none of this. Reading the first slice with file.slice(0, 1).arrayBuffer() before accepting the file catches it. Broadening accept to "video/*,audio/*", or dropping it entirely, makes Android open the system file manager instead and reduces how often it happens.

Step 4: Hand the file to something resumable

A drop zone's job ends when it produces a File. What happens next is where video diverges from every other upload. A single request carrying 3 GB won't survive a network blip, and has no way to resume.

text
import { Uploader } from "@fastpix/resumable-uploads";

function startUpload(file, signedUrl) {
  const upload = Uploader.init({ endpoint: signedUrl, file, chunkSize: 5120 });

  upload.on("progress", (e) => setProgress(e.detail.progress));
  upload.on("offline", () => setNote("Connection lost. The upload resumes automatically."));
  upload.on("success", () => setDone(true));

  return upload;
}

The signed URL comes from your server, not the browser, because minting it needs your Secret Key. The React (Vite) guide has the Express route, and resumable uploads for web has the full event surface. We covered the chunking model itself in uploading large video files with chunking.

Or use the drop zone that already ships with the uploader

All of the above is packaged in @fastpix/fp-react-uploader. FastPixDropZone is a composable child of FastPixUploader. You keep control of layout and lose the event bookkeeping:

text
import {
  FastPixUploader,
  FastPixDropZone,
  FastPixStatus,
  FastPixTrack,
} from "@fastpix/fp-react-uploader";
import "@fastpix/fp-react-uploader/styles.css";

<FastPixUploader endpoint={createUpload} accept="video/*" maxFileSize={2_000_000}>
  <FastPixDropZone overlay>
    <p>Drag a video here, or click to browse</p>
  </FastPixDropZone>
  <FastPixStatus />
  <FastPixTrack showLabel />
</FastPixUploader>

Rejections arrive through onFileReject. The reason is "type", "size", "unreadable" or "busy", plus a message you can display directly. The "unreadable" case is the Android situation from Step 3, and the readability check runs whether or not you set accept. Styling is CSS variables rather than a styling library, and the full list of --fastpix-* variables is in the uploader README.

Keyboard and screen reader access

A drop zone that only responds to a pointer excludes anyone who doesn't use one, and drag and drop has no keyboard equivalent at all. So the zone has to double as a button.

Make it a real <button> rather than a <div> with role="button". That gets you focus, Enter and Space activation, and the focus ring for free, and clicking it should open the file dialog. Announce state changes through a live region, so a screen reader user hears progress and completion instead of watching a bar they can't see. FastPixDropZone ships as a button with an overridable label, and FastPixStatus renders with role="status" and aria-live="polite".

One placement rule catches people out: don't nest FastPixFilePicker inside FastPixDropZone. The drop zone already opens the file dialog when activated, so a button inside it produces two dialogs.

Drop a video into your React app with FastPix

Install @fastpix/fp-react-uploader, render FastPixDropZone inside FastPixUploader, and point endpoint at a route that returns a signed upload URL. Dropping a file starts a chunked upload immediately, with progress, pause and resume already wired. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. That's enough to test the folder drop, the size rejection and the offline resume before you write a single handler.

Frequently Asked Questions (FAQs)

Why does my React dropzone flicker when dragging over it?

Because dragleave fires every time the pointer crosses into a child element, not only when it leaves the zone. Tracking active state with a boolean therefore turns the highlight off mid-drag. Keep a depth counter in a ref. Increment on dragenter, decrement on dragleave, and treat the zone as active only while the count is above zero.

Do I need a library for drag and drop file upload in React?

No. Four event handlers and a depth counter cover the browser behavior, which is roughly forty lines. A library earns its place once you also need validation, progress, retries and accessible controls. At that point, an uploader component with a drop zone built in saves more than a drag and drop library alone.

How do I handle folders dropped on a React dropzone?

Read event.dataTransfer.items instead of event.dataTransfer.files, and call webkitGetAsEntry() on each item to check isDirectory. The files list silently omits directories, so a folder drop appears to do nothing. For video, telling the user to drop the files rather than the folder is usually better than recursing into it.

Can a React dropzone upload files larger than 2 GB?

The drop zone itself has no size limit because it only produces a File reference. Whether the upload survives depends entirely on what you do next: a single request that large will not survive a network interruption. Chunked resumable uploads send 5 MB to 500 MB slices and resume from the last completed chunk.

How do I make a drag and drop upload keyboard accessible?

Render the zone as a real <button>, so it is focusable and activates on Enter or Space. Have that activation open the file dialog. Drag and drop has no keyboard equivalent, so the picker is the accessible path rather than an extra. Announce upload state through an element with role="status" and aria-live="polite".

Share

Stay Ahead of Video
Streaming Trends

Start shipping video today.