HTML Drag and Drop API: Complete Guide to Drag & Drop

The HTML Drag and Drop API lets users pick up an element with a pointer device and move it to another target in the page. It is useful for sortable lists, file-like workflows, dashboards, and other interfaces where direct manipulation feels more natural than clicking buttons.

Quick answer: Make an element draggable with draggable="true", listen for dragstart to set data, call preventDefault() in dragover on the drop target, and handle the drop event to read the dragged data.

Difficulty: Intermediate

You'll understand this better if you know: basic HTML elements, event handling, and how browser events work in a typical web page.

1. What Is the HTML Drag and Drop API?

The HTML Drag and Drop API is a browser feature for moving data or elements from one place to another by dragging with a mouse, trackpad, or similar pointer. It is part of the browser's built-in event system and uses HTML attributes and events rather than custom libraries.

The most important idea is that the dragged element does not magically move itself. You usually store some data during dragstart, allow a target to accept the drop during dragover, and then update the page in drop.

2. Why the HTML Drag and Drop API Matters

Drag and drop gives users a direct way to reorganize content or transfer information. It is especially useful when the spatial arrangement matters, such as moving items between columns or sorting tasks.

It matters because it can reduce the number of clicks, make interfaces feel more natural, and support workflows where order or grouping is important. For example, a Kanban board, a file organizer, or a bookmark manager can benefit from drag and drop.

It is not always the best choice, though. If the task is simple and keyboard-first navigation is important, buttons, menus, or form controls may be easier to understand and more accessible.

3. Basic Syntax or Core Idea

The core pattern is simple: mark the source as draggable, capture the dragged data, permit dropping, and handle the final drop action.

Minimal structure

The draggable element uses draggable="true". The drop target listens for dragover and drop. In the dragover handler, you must usually call preventDefault() or the browser will not allow dropping.

<div draggable="true" id="item">Drag me</div>
<div id="dropzone">Drop here</div>

This HTML only declares the source and the target. The behavior comes from drag events attached in script, and the browser provides a DataTransfer object for passing information between them.

Core event flow

  1. dragstart fires on the draggable source.
  2. You store information with dataTransfer.setData().
  3. dragover fires repeatedly on potential drop targets.
  4. You call preventDefault() so dropping is allowed.
  5. drop fires when the user releases the item over a valid target.
  6. You read the data with dataTransfer.getData() and update the page.

4. Step-by-Step Examples

Example 1: Drag text from one box to another

This example shows the simplest useful flow: store a string in dragstart and read it in drop.

<div id="source" draggable="true">Write a note</div>
<div id="target" aria-label="Drop zone">Drop here</div>

On the source, set the payload during drag start. On the target, allow the drop and show the received text.

const source = document.getElementById("source");
const target = document.getElementById("target");

source.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", source.textContent);
});

target.addEventListener("dragover", (event) => {
  event.preventDefault();
});

target.addEventListener("drop", (event) => {
  event.preventDefault();
  const text = event.dataTransfer.getData("text/plain");
  target.textContent = text;
});

This example demonstrates the minimum pieces needed for text-based drag and drop. The data is set once, allowed over the target, and read back when dropped.

Example 2: Reorder list items

A common real use case is sorting items in a list. Here, each item can be dragged and dropped before another item.

<ul id="list">
  <li draggable="true" data-id="1">Plan</li>
  <li draggable="true" data-id="2">Build</li>
  <li draggable="true" data-id="3">Test</li>
</ul>

When the user starts dragging, store the item's identifier. When the item is dropped on another item, move it in the DOM.

const list = document.getElementById("list");
let draggedItem = null;

list.addEventListener("dragstart", (event) => {
  draggedItem = event.target;
  event.dataTransfer.setData("text/plain", draggedItem.dataset.id);
});

list.addEventListener("dragover", (event) => {
  event.preventDefault();
});

list.addEventListener("drop", (event) => {
  event.preventDefault();
  const dropTarget = event.target.closest("li");

  if (draggedItem && dropTarget && draggedItem !== dropTarget) {
    list.insertBefore(draggedItem, dropTarget);
  }
});

This pattern is common for sortable lists. The important part is that the drop target is resolved carefully, because the event may originate from nested elements inside the list item.

Example 3: Use drag feedback for a card interface

Users often need visual feedback while dragging. The browser can show a drag image automatically, but you can also update classes while the drag is in progress.

<article class="card" draggable="true" id="card-a">
  <h3>Project Brief</h3>
  <p>Move me to another column.</p>
</article>

Here, the card changes state during dragging and resets afterward.

const card = document.getElementById("card-a");

card.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", card.id);
  card.classList.add("is-dragging");
});

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

This example shows how drag feedback can improve clarity without changing the core drag-and-drop logic.

Example 4: Transfer text between two separate regions

You can use the API to move simple content between two panels on the page. This is useful for note boards, tag pickers, or custom builders.

<section id="available">
  <p draggable="true">Archived note</p>
</section>
<section id="archive">
  <p>Drop archived notes here</p>
</section>

The target accepts drops and appends the dropped content or its text to a new location.

const available = document.getElementById("available");
const archive = document.getElementById("archive");

available.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", event.target.textContent.trim());
});

archive.addEventListener("dragover", (event) => {
  event.preventDefault();
});

archive.addEventListener("drop", (event) => {
  event.preventDefault();
  const note = event.dataTransfer.getData("text/plain");
  archive.insertAdjacentHTML("beforeend", `<p>${note}</p>`);
});

This shows a common pattern for building custom move targets. In a real app, you would usually sanitize any data before inserting markup and prefer safer DOM methods when possible.

5. Practical Use Cases

These use cases work well when the user benefits from seeing where content will land. They are less useful when the action is purely symbolic or when keyboard-only workflows must be the main path.

6. Common Mistakes

Mistake 1: Forgetting to allow the drop with dragover

Many developers add a drop handler but forget that the browser blocks dropping unless the target cancels the default dragover behavior.

Problem: The drop event never fires because the drop zone is not marked as a valid target.

const zone = document.getElementById("zone");

zone.addEventListener("drop", (event) => {
  const value = event.dataTransfer.getData("text/plain");
  zone.textContent = value;
});

Fix: Add a dragover handler and call preventDefault().

zone.addEventListener("dragover", (event) => {
  event.preventDefault();
});

zone.addEventListener("drop", (event) => {
  event.preventDefault();
  const value = event.dataTransfer.getData("text/plain");
  zone.textContent = value;
});

The corrected version works because the browser now treats the element as a valid drop target.

Mistake 2: Reading data from the wrong place

The dragged payload lives on the DataTransfer object, not on the source element itself. New developers sometimes try to read it from a custom property on the element.

Problem: The drop handler receives no data because the information was never stored where the browser expects it.

const item = document.getElementById("item");
const zone = document.getElementById("zone");

item.addEventListener("dragstart", () => {
  item.payload = "Hello";
});

zone.addEventListener("drop", () => {
  zone.textContent = item.payload;
});

Fix: Store and read the payload through event.dataTransfer.

item.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", "Hello");
});

zone.addEventListener("drop", (event) => {
  event.preventDefault();
  zone.textContent = event.dataTransfer.getData("text/plain");
});

The fixed version works because the browser passes the payload through the drag data store.

Mistake 3: Using HTML insertion with untrusted dragged text

When handling dropped text, some developers inject it directly as markup. That can create broken layouts or introduce security risks if the data came from outside the page.

Problem: The code treats dragged text as HTML, so any markup in the dropped value is interpreted by the browser.

const output = document.getElementById("output");

output.addEventListener("drop", (event) => {
  output.innerHTML = event.dataTransfer.getData("text/plain");
});

Fix: Use textContent for plain text or create DOM nodes with safe APIs.

output.addEventListener("drop", (event) => {
  event.preventDefault();
  output.textContent = event.dataTransfer.getData("text/plain");
});

The corrected version works because plain text is inserted as text, not parsed as HTML.

7. Best Practices

Practice 1: Use accessible labels and clear drop targets

Drag and drop should be understandable without relying only on visual cues. Give drop zones clear names and supplement drag interactions with text instructions.

<div id="dropzone" role="region" aria-label="Archive notes drop zone">
  <p>Drag a note here to archive it.</p>
</div>

This makes the target easier to announce to assistive technology and clearer for sighted users too.

Practice 2: Keep dragged data small and specific

Pass an identifier or small text token instead of a large blob of state. The drop target can then look up the full record if needed.

card.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", card.dataset.cardId);
});

This approach is easier to debug and reduces the chance of stale or oversized drag data.

Practice 3: Provide a non-drag alternative

Not every user can or wants to drag items. Add buttons such as Move up, Move down, or Move to archive so the task can still be completed.

<button type="button">Move up</button>
<button type="button">Move down</button>

This improves accessibility and gives users a fallback when drag gestures are inconvenient or unavailable.

8. Limitations and Edge Cases

One common "not working" complaint is that the dragged item seems to disappear or no drop occurs on mobile devices. That is usually a platform limitation or a missing touch-specific implementation, not a problem with your HTML alone.

9. Practical Mini Project

Here is a small working example of a simple todo sorter. It lets the user drag a task from one place in the list to another and reorders the list in place.

<section aria-labelledby="tasks-title">
  <h2 id="tasks-title">Tasks</h2>
  <ul id="tasks">
    <li draggable="true" data-id="task-1">Write outline</li>
    <li draggable="true" data-id="task-2">Review notes</li>
    <li draggable="true" data-id="task-3">Publish draft</li>
  </ul>
</section>
const tasks = document.getElementById("tasks");
let draggedTask = null;

tasks.addEventListener("dragstart", (event) => {
  const item = event.target.closest("li");
  if (!item) return;

  draggedTask = item;
  event.dataTransfer.setData("text/plain", item.dataset.id);
  item.classList.add("is-dragging");
});

tasks.addEventListener("dragover", (event) => {
  event.preventDefault();

  const targetItem = event.target.closest("li");
  if (!targetItem || !draggedTask || targetItem === draggedTask) {
    return;
  }

  const rect = targetItem.getBoundingClientRect();
  const after = event.clientY > rect.top + rect.height / 2;
  if (after) {
    targetItem.after(draggedTask);
  } else {
    targetItem.before(draggedTask);
  }
});

tasks.addEventListener("dragend", (event) => {
  const item = event.target.closest("li");
  if (item) {
    item.classList.remove("is-dragging");
  }
  draggedTask = null;
});

This mini project shows the complete loop: mark the items draggable, allow dropping, determine the insertion point, and clean up when the drag ends. It is a practical starting point for sortable interfaces.

10. Key Points

11. Practice Exercise

Expected output: The order of the items changes when you drag one item and release it over another.

Hint: Use closest("li") so you always work with the actual list item even if the user drops on nested content.

<ul id="exercise-list">
  <li draggable="true" data-id="a">Alpha</li>
  <li draggable="true" data-id="b">Beta</li>
  <li draggable="true" data-id="c">Gamma</li>
</ul>

<script>
const exerciseList = document.getElementById("exercise-list");
let activeItem = null;

exerciseList.addEventListener("dragstart", (event) => {
  const item = event.target.closest("li");
  if (!item) return;
  activeItem = item;
  event.dataTransfer.setData("text/plain", item.dataset.id);
});

exerciseList.addEventListener("dragover", (event) => {
  event.preventDefault();

  const targetItem = event.target.closest("li");
  if (!targetItem || !activeItem || targetItem === activeItem) return;

  const rect = targetItem.getBoundingClientRect();
  const after = event.clientY > rect.top + rect.height / 2;

  if (after) {
    targetItem.after(activeItem);
  } else {
    targetItem.before(activeItem);
  }
});

exerciseList.addEventListener("dragend", () => {
  activeItem = null;
});
</script>

12. Final Summary

The HTML Drag and Drop API is a built-in browser feature for moving data or elements between sources and targets. Its central workflow is straightforward: make something draggable, store data on drag start, allow the drop target during drag over, and process the result on drop.

It is a strong fit for sortable interfaces and direct-manipulation workflows, but it comes with real limitations. Accessibility, touch support, and safe data handling all need careful attention if you want the feature to work well for real users.

If you want to go further, the next step is to build a sortable list or board with keyboard-friendly controls alongside drag and drop so you can support more users and more devices.