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.
- It works with draggable sources, drop targets, and a shared data payload.
- It uses events such as dragstart, dragover, drop, and dragend.
- It can move text, identifiers, or file-like data between elements and browser features.
- It is built into HTML and the browser, so you do not need a framework to use it.
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
- dragstart fires on the draggable source.
- You store information with dataTransfer.setData().
- dragover fires repeatedly on potential drop targets.
- You call preventDefault() so dropping is allowed.
- drop fires when the user releases the item over a valid target.
- 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
- Reordering items in a playlist, task list, or navigation builder.
- Moving cards between columns in a Kanban-style board.
- Dragging snippets, labels, or tags into a composed message or form.
- Building custom editors where blocks can be rearranged.
- Transferring items between two panes, such as available versus selected items.
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
- The API is most reliable with mouse and trackpad interaction; touch support is inconsistent across browsers and often needs extra handling.
- Dragging selected text, links, or images can behave differently from dragging custom elements because browsers provide default drag behavior for some content.
- Some event properties are read-only or limited during certain phases, so you should set drag data during dragstart rather than later.
- Nested elements can confuse target resolution, so you may need closest() to find the actual drop item.
- Default browser behavior may show a not-allowed cursor or refuse to drop unless the target handles dragover correctly.
- Data transfer formats matter. If you store data as text/plain and read a different type, the result may look empty even though data was set.
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
- The HTML Drag and Drop API is browser-native and event-driven.
- draggable="true" marks the drag source.
- dragover must usually call preventDefault() to permit a drop.
- DataTransfer carries the dragged data between events.
- It works well for reordering and moving items, but accessibility and touch support need extra care.
11. Practice Exercise
- Create a three-item list where each item can be dragged above or below the others.
- Store a unique ID for each item during dragstart.
- Allow the list to accept drops by handling dragover.
- Move the dropped item to its new position using DOM methods such as before or after.
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.