← thecodex.expert · The Codex Family of Knowledge
The Codex / JavaScript / Drag & Drop

Drag & Drop

Native HTML5 drag and drop — draggable elements, drop targets, DataTransfer, file drops, and sorting lists.

What it is, in plain English: The HTML5 Drag and Drop API lets users drag elements around the page and drop them on designated targets. The dragging element fires dragstart, drag, and dragend events. The drop target fires dragenter, dragover, dragleave, and drop events. DataTransfer carries the data from drag source to drop target. File drops (from the OS) use the same API — files arrive in event.dataTransfer.files.
Where this bites you

dragleave doesn't mean "the drag left the drop zone". It fires on the drop zone every time the pointer moves onto one of its children, and the child's own dragleave bubbles up as well. Any drop zone with a label or an icon inside it gets a stream of false "left" events, so the highlight flickers. If dragleave hides the zone or resets state, it can vanish right under the cursor.

// <div id="drop-target"><p>Drop here</p></div>   <- a child element inside the target
const dropTarget = document.getElementById("drop-target");

dropTarget.addEventListener("dragover", (e) => {
  e.preventDefault();
  dropTarget.classList.add("drag-over");
});
dropTarget.addEventListener("dragleave", (e) => {
  dropTarget.classList.remove("drag-over");     // runs while the pointer is still inside
  console.log("dragleave fired, pointer now over:", e.relatedTarget?.tagName);
});
// drag into the target, over the <p>, then out:
// dragleave fired, pointer now over: P      <- still inside the drop target
// dragleave fired, pointer now over: DIV    <- bubbled up from the <p>
// dragleave fired, pointer now over: HTML   <- the only real exit

Making elements draggable

Basic drag and drop

<!-- HTML: add draggable="true" to make an element draggable -->
<div draggable="true" id="card">Drag me!</div>
<div id="drop-target">Drop here</div>
const card       = document.getElementById("card");
const dropTarget = document.getElementById("drop-target");

// ── DRAGGABLE ELEMENT EVENTS ──────────────────────────────────────────────
card.addEventListener("dragstart", (event) => {
  // Store data to pass to the drop target:
  event.dataTransfer.setData("text/plain", card.id);
  event.dataTransfer.effectAllowed = "move";  // "copy", "move", "link"
  card.classList.add("dragging");
});

card.addEventListener("dragend", () => {
  card.classList.remove("dragging");
});

// ── DROP TARGET EVENTS ────────────────────────────────────────────────────
dropTarget.addEventListener("dragover", (event) => {
  event.preventDefault();  // ← REQUIRED or drop won't fire!
  event.dataTransfer.dropEffect = "move";
  dropTarget.classList.add("drag-over");
});

dropTarget.addEventListener("dragleave", () => {
  dropTarget.classList.remove("drag-over");
});

dropTarget.addEventListener("drop", (event) => {
  event.preventDefault();
  const id      = event.dataTransfer.getData("text/plain");
  const dragged = document.getElementById(id);
  dropTarget.appendChild(dragged);   // move the element
  dropTarget.classList.remove("drag-over");
});

The basic example above has the same pattern, so its highlight flickers as soon as the drop target contains anything. dragleave tells you where the pointer went, in event.relatedTarget. If that's still inside the drop zone, ignore the event:

const dropTarget = document.getElementById("drop-target");

dropTarget.addEventListener("dragover", (e) => {
  e.preventDefault();
  dropTarget.classList.add("drag-over");
});
dropTarget.addEventListener("dragleave", (e) => {
  if (dropTarget.contains(e.relatedTarget)) return;   // moved onto a child: still inside
  dropTarget.classList.remove("drag-over");
  console.log("left the drop target");
});
// same drag: one message, only when the pointer really leaves
// left the drop target

File drop — dragging files from the desktop

dropZone.addEventListener("dragover", (e) => {
  e.preventDefault();
  dropZone.classList.add("active");
});

dropZone.addEventListener("drop", (e) => {
  e.preventDefault();
  dropZone.classList.remove("active");

  // Access dropped files:
  const files = [...e.dataTransfer.files];
  files.forEach(file => {
    console.log(`File: ${file.name} (${file.type}, ${(file.size/1024).toFixed(1)}KB)`);

    if (file.type.startsWith("image/")) {
      const reader = new FileReader();
      reader.onload = (e) => { preview.src = e.target.result; };
      reader.readAsDataURL(file);
    }
  });
});
Try It Yourself

Official Sources

The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.