Community JavaScript Snippet
A File Drop Zone Without a Library
The drop zone I keep instead of pulling in react-dropzone (60+kB minified): drag-over visuals, multi-file drops, folder uploads via DataTransferItem, and the Safari quirk where dragleave fires on every child enter.
A File Drop Zone Without a Library
The drop zone I keep instead of pulling in react-dropzone (60+kB minified): drag-over visuals, multi-file drops, folder uploads via DataTransferItem, and the Safari quirk where dragleave fires on every child enter.
By @isabellarashid
December 6, 2025
·
Updated May 18, 2026
323 views
1
4.4 (13)
The depth counter is the part nobody warns you about. The MDN docs show a single boolean isOver, which works fine until your zone has a button or icon inside it; then every hover over the inner element fires dragleave on the parent and your highlight flashes. Counting dragenter minus dragleave and only flipping the visual at the boundary makes the highlight stable. The dragover listener calling preventDefault() is non-negotiable: without it the browser's default behavior is dropEffect = 'none', so the cursor shows the no-drop icon and drop never fires. The synthetic harness at the bottom is how I unit-test the FSM in CI without spinning up a browser.
The folder path is what users always assume should work and what almost never does out of the box. dataTransfer.files gives you only the files dropped at the top level, so dropping photos/ gives an empty list. The webkitGetAsEntry API (now part of the File and Directory Entries spec, prefix kept for backwards compat) is the way through. Walking it requires readEntries, which is paginated: it returns up to 100 entries per call and an empty array signals "done", which is why the loop runs until the batch is empty. The fallback path keeps the zone working in browsers that lack the API (Firefox added it in 50, Safari in 11.1).
I prefer per-file rejection reasons over a single "upload failed" message because users want to know which file is wrong. The MIME check uses f.type from the browser, which is set from the file extension on Windows and from the actual content-type sniff on macOS, so it is best-effort, not a security boundary. For real security we re-validate on the server with magic bytes; the client check is a UX layer that catches 95% of mistakes without a round trip. The size check happens in JS-land before any byte is read, so a 4GB drop fails instantly instead of hanging the page on an arrayBuffer() call.
I added the relatedTarget guard after a Safari user reported the upload affordance flickering on every hover. In Safari the event order on entering a child is dragleave (parent, relatedTarget=child) -> dragenter (parent, relatedTarget=oldChild), so the depth counter dips negative for a frame and the visual flickers. Checking whether relatedTarget is contained in the zone short-circuits both events and keeps the visual stable. Chrome and Firefox emit the events in the more sensible dragenter (child) -> dragleave (parent, relatedTarget=child) order, so the guard is a no-op there. This is the entire reason I keep my own drop zone instead of pulling in react-dropzone: the bug surface is small enough to own.
