JavaScript Drag and Drop Table

PieceWhat it doesField note
dragstart / dragover / dropThe event spinedragend fires on the SOURCE; dragleave lies at child boundaries
e.preventDefault() in dragoverALLOW the dropNo preventDefault = browser navigates to the file - the classic footgun
e.dataTransfer.setDataCarry payloadTypes are LOWERCASE; getData only works in drop (read at start = empty)
e.dataTransfer.filesDropped filesitems vs files: items give kind/type before drop; files after
effectAllowed / dropEffectCursor semanticscopy/move/link - the user sees what WILL happen
dragenter + counterStable hover statedragleave fires entering children - enter/leave counter is the fix
dropEffect = 'copy'The feedback halfSet it IN dragover - cursor changes only when both sides agree
keyboard alternativeNever drag-onlyA file input beside the zone - drag is enhancement, not the door
Reference: the MDN drag and drop reference. Drag and drop is an event protocol with one non-negotiable: calling preventDefault() in dragover is what MARKS the target as a drop zone - skip it and dropping a file makes the BROWSER navigate to that file (the classic self-destruct). The second famous trap: dragleave fires every time the pointer crosses INTO a child element, so the highlight flickers - the enter/leave counter is the standard fix. Bottom line: payload via dataTransfer (setData types are lowercase; getData works only at drop), cursor semantics via effectAllowed/dropEffect agreement, and always a file-input alternative because drag is enhancement, never the only door. Related tools: file API table (what lands in dataTransfer.files), event listeners table (the event protocol beneath), and form table (the always-present keyboard alternative).

HTML drag and drop is an event protocol with one non-negotiable law: calling preventDefault() inside dragover is what MARKS an element as a drop zone. Skip it, and dropping a file onto your page makes the BROWSER navigate to that file - the classic self-destruct that every drag-drop tutorial opens with.

Bottom line: the second famous trap is flicker - dragleave fires every time the pointer crosses INTO a child element, so the highlight stutters over anything with nested nodes. The enter/leave counter is the standard fix: increment on dragenter, decrement on dragleave, highlight while the counter is positive.

The honest part: the payload timing is asymmetric by design - setData works at dragstart, but getData returns empty everywhere except drop (a security rule against drag-time snooping), and dropped files only materialize in dataTransfer.files AT the drop. Plan your handlers around what each event is allowed to know.

How to use

  1. Mark the zone: zone.addEventListener('dragover', e => e.preventDefault()) - this single line turns navigation-into-the-void into an actual drop target.
  2. Fix the flicker: let depth = 0; dragenter => depth++, dragleave => depth--, drop/dragleave(when 0) => unhighlight - children stop breaking the state.
  3. Read the files at drop only: e.dataTransfer.files in the drop handler feeds the File API - the same objects an input would have delivered.

Frequently asked questions

Why does dropping a file on my page open the file in the browser?

Because nothing claimed the drop. The browser's default behavior for an unclaimed file drop is to NAVIGATE to it (rendering the PDF or image, discarding your app state) - the same default that dragover's preventDefault overrides. This is why the drop zone needs BOTH: preventDefault in dragover (allow, and show the cursor semantics) AND preventDefault in drop (stop the navigation at the moment of truth). One without the other fails - allow without handling, or handle without allowing. The related heartbreak: the user who drops a file on the WRONG part of your SPA loses their form - large apps globally preventDefault document-level drops for exactly this reason.

Why does my drop highlight flicker when hovering over child elements?

dragleave's precision is the problem: it fires every time the pointer moves from the zone ONTO one of its children (leaving the zone, entering the child), then dragenter fires again - so a highlight toggled on these events alone strobes across any non-trivial markup. The enter/leave counter is the canonical fix: dragenter increments, dragleave decrements, and the highlight tracks 'counter > 0' - children push the count up and back down without crossing zero. The modern alternative: pointer-events: none on the zone's children during drag (CSS class toggled at dragenter), so children become transparent to the drag events entirely.

When can I read the drag payload, and why the restriction?

At the drop, effectively. setData at dragstart writes keyed strings (types lowercase: 'text/plain', custom keys), but getData returns empty outside drop for non-same-origin content - a security rule preventing the dragging PAGE (or an intermediate hover target) from reading data the user never confirmed dropping. Files follow the same timing: dataTransfer.files is empty during dragover and populated at drop (the items list offers kind/type earlier for hover UX). Design accordingly: dragover may show 'drop to upload' affordances but cannot know the filename; all real work starts in the drop handler.

What do effectAllowed and dropEffect actually control?

The user-visible contract. effectAllowed is set at dragstart by the SOURCE (copy, move, link or combinations) - what operations it permits. dropEffect is set by the TARGET during dragover - what it plans to do. The cursor reflects their AGREEMENT: matching copy shows the copy cursor; a target wanting move when the source allows only copy shows the not-allowed cursor. Beyond the cursor, the semantics are advisory - the actual copy-or-move behavior is your code in drop and dragend. Their real value is communication: a well-behaved pair tells the user what will happen before they release, which is half of what makes drag feel trustworthy.

Related tools