|$ curl https://forge-ai.dev/api/markdown?path=docs/html/drag-drop
$cat docs/html-drag-&-drop-api.md
updated Recently·25 min read·published

HTML Drag & Drop API

HTMLDrag & DropAPIIntermediate
Introduction

The HTML Drag and Drop API enables interactive drag-and-drop functionality natively in the browser. It allows users to click and hold on an element, drag it to another location, and release to drop it. The API supports both internal elements (dragging within a page) and external sources (dragging files from the operating system).

The system is event-driven, with distinct events for both the draggable element and the drop target. The DataTransfer object carries data between drag source and drop target, supporting multiple data types and files.

Drag and drop consists of three phases: drag start (source prepares data), drag over (potential target accepts or rejects), and drop (target receives data). The API works on desktop browsers universally, with limited mobile support requiring touch event fallbacks.

Draggable Attribute

The draggable attribute makes an HTML element draggable. By default, only images, links, and selected text are draggable. All other elements require draggable="true".

ValueEffect
trueElement is draggable
falseElement is not draggable
autoBrowser default behavior (default)
draggable-attribute.html
HTML
1<!-- Make a div draggable -->
2<div draggable="true" id="drag1" class="draggable-item">
3 Drag me
4</div>
5
6<!-- Links and images are draggable by default -->
7<a href="https://example.com">Draggable link</a>
8<img src="photo.jpg" alt="Draggable image" />
9
10<!-- Prevent dragging on an image -->
11<img src="icon.svg" alt="Not draggable" draggable="false" />
12
13<!-- List items as drag sources -->
14<ul>
15 <li draggable="true" data-id="1">Task: Review PR</li>
16 <li draggable="true" data-id="2">Task: Write tests</li>
17 <li draggable="true" data-id="3">Task: Deploy to staging</li>
18</ul>

info

Setting draggable="true" alone is not enough. You must also handle the dragstart event to set data via dataTransfer.setData(). Without data, the drag operation may be canceled by the browser.
Drag Events

The drag lifecycle consists of seven events. Some fire on the drag source (draggable element), others on the drop target, and some on both.

EventFires OnDescription
dragstartSourceDrag operation begins — set data and drag image
dragSourceContinuously fires while dragging
dragendSourceDrag operation ends (drop or cancel)
dragenterTargetDragged element enters a valid target
dragoverTargetContinuously fires while over target (must prevent default)
dragleaveTargetDragged element leaves the target
dropTargetElement is dropped on the target — receive data
drag-events.js
JavaScript
1// === Source element events ===
2
3let draggedItem = null;
4
5document.addEventListener("dragstart", (e) => {
6 const el = e.target.closest("[draggable]");
7 if (!el) return;
8
9 draggedItem = el;
10 e.dataTransfer.setData("text/plain", el.textContent);
11 e.dataTransfer.effectAllowed = "move";
12 el.classList.add("dragging");
13});
14
15document.addEventListener("drag", (e) => {
16 // Fires many times — avoid heavy computations here
17});
18
19document.addEventListener("dragend", (e) => {
20 const el = e.target.closest("[draggable]");
21 if (el) el.classList.remove("dragging");
22 draggedItem = null;
23});
24
25// === Target element events ===
26
27document.addEventListener("dragenter", (e) => {
28 const target = e.target.closest(".drop-zone");
29 if (!target) return;
30 target.classList.add("drag-over");
31});
32
33document.addEventListener("dragover", (e) => {
34 const target = e.target.closest(".drop-zone");
35 if (!target) return;
36 e.preventDefault(); // Required to allow drop
37 e.dataTransfer.dropEffect = "move";
38});
39
40document.addEventListener("dragleave", (e) => {
41 const target = e.target.closest(".drop-zone");
42 if (!target) return;
43 target.classList.remove("drag-over");
44});
45
46document.addEventListener("drop", (e) => {
47 e.preventDefault();
48 const target = e.target.closest(".drop-zone");
49 if (!target || !draggedItem) return;
50
51 target.classList.remove("drag-over");
52 const data = e.dataTransfer.getData("text/plain");
53 target.textContent = "Dropped: " + data;
54});

warning

The dragover event must call e.preventDefault() — without this, the drop event will never fire. This is the most common bug when implementing drag and drop. Similarly, drop events also need e.preventDefault() to prevent the browser's default handling (e.g., opening a link).
DataTransfer

The DataTransfer object is the bridge between drag source and drop target. It carries data, files, and drag metadata throughout the drag session.

Method / PropertyDescription
setData(format, data)Sets drag data for a specific MIME type or custom format
getData(format)Retrieves drag data for a given format
typesArray of data formats set on the drag
filesFileList of dropped files (from OS)
effectAllowedAllowed operations: none, copy, move, link, all
dropEffectCurrent operation feedback: none, copy, move, link
clearData(format)Removes data for a format (or all if format omitted)
setDragImage(img, x, y)Sets a custom image shown during drag
datatransfer.js
JavaScript
1// Setting multiple data formats
2element.addEventListener("dragstart", (e) => {
3 e.dataTransfer.setData("text/plain", "Item name");
4 e.dataTransfer.setData("text/html", "<strong>Item name</strong>");
5 e.dataTransfer.setData("application/json", JSON.stringify({
6 id: 42,
7 name: "Item name",
8 category: "widget"
9 }));
10 e.dataTransfer.effectAllowed = "copy";
11});
12
13// Reading data on drop
14target.addEventListener("drop", (e) => {
15 e.preventDefault();
16
17 // Check what formats are available
18 console.log("Available types:", e.dataTransfer.types);
19
20 if (e.dataTransfer.types.includes("application/json")) {
21 const raw = e.dataTransfer.getData("application/json");
22 const item = JSON.parse(raw);
23 console.log("Received item:", item);
24 } else {
25 const text = e.dataTransfer.getData("text/plain");
26 console.log("Received text:", text);
27 }
28});
29
30// effectAllowed vs dropEffect
31// effectAllowed — set on dragstart (what the source allows)
32// dropEffect — set on dragover (what the target requests)
33// Browser shows appropriate cursor based on the intersection
34
35target.addEventListener("dragover", (e) => {
36 e.preventDefault();
37 e.dataTransfer.dropEffect = "copy"; // shows "+" cursor
38});
🔥

pro tip

Use custom MIME types like application/json to pass structured data. The text/plain format should always be set as a fallback for accessibility and external target compatibility. The combination of effectAllowed (source) and dropEffect (target) determines the cursor icon the browser shows.
Drag Feedback

Custom drag feedback improves the user experience. The setDragImage method sets a custom image shown under the cursor. You can also style elements during drag phases to provide visual cues.

drag-feedback.js
JavaScript
1// Custom drag image from an element
2element.addEventListener("dragstart", (e) => {
3 // Create a custom drag image
4 const ghost = document.createElement("div");
5 ghost.textContent = e.target.textContent;
6 ghost.style.cssText = `
7 background: rgba(0, 255, 65, 0.15);
8 border: 1px solid #00FF41;
9 border-radius: 6px;
10 padding: 8px 14px;
11 font-family: monospace;
12 font-size: 12px;
13 color: #00FF41;
14 transform: rotate(-3deg);
15 `;
16 document.body.appendChild(ghost);
17 e.dataTransfer.setDragImage(ghost, 10, 10);
18
19 // Clean up after drag starts (the browser clones the element)
20 setTimeout(() => document.body.removeChild(ghost), 0);
21});
22
23// Visual feedback during drag
24const style = document.createElement("style");
25style.textContent = `
26 .dragging {
27 opacity: 0.4;
28 outline: 2px dashed #00FF41;
29 }
30 .drop-zone.drag-over {
31 background: rgba(0, 255, 65, 0.08);
32 border-color: #00FF41;
33 }
34 .drop-zone.drag-over * {
35 pointer-events: none;
36 }
37`;
38document.head.appendChild(style);

info

The setDragImage method accepts any DOM element — the browser renders a snapshot. Create a temporary element with styling, set it as the drag image, then remove it from the DOM immediately. The browser clones the snapshot internally, so removal is safe. For consistent cross-browser behavior, keep drag images to a modest size (under 300x300px).
File Drag & Drop

Files can be dragged directly from the operating system onto a web page. The dataTransfer.files property provides access to the dropped files as a FileList.

file-drag-drop.js
JavaScript
1const dropZone = document.getElementById("file-drop-zone");
2
3// Prevent browser default (opening the file)
4["dragenter", "dragover", "dragleave", "drop"].forEach((event) => {
5 dropZone.addEventListener(event, (e) => {
6 e.preventDefault();
7 e.stopPropagation();
8 });
9});
10
11dropZone.addEventListener("dragenter", () => {
12 dropZone.classList.add("highlight");
13});
14
15dropZone.addEventListener("dragleave", () => {
16 dropZone.classList.remove("highlight");
17});
18
19dropZone.addEventListener("drop", (e) => {
20 dropZone.classList.remove("highlight");
21
22 const files = Array.from(e.dataTransfer.files);
23
24 if (files.length === 0) return;
25
26 files.forEach((file) => {
27 console.log("File:", file.name);
28 console.log(" Type:", file.type);
29 console.log(" Size:", formatFileSize(file.size));
30
31 // Read file contents
32 const reader = new FileReader();
33
34 if (file.type.startsWith("image/")) {
35 reader.onload = (ev) => {
36 const img = document.createElement("img");
37 img.src = ev.target.result;
38 dropZone.appendChild(img);
39 };
40 reader.readAsDataURL(file);
41 } else if (file.type === "text/csv" || file.type === "text/plain") {
42 reader.onload = (ev) => {
43 processFileData(ev.target.result);
44 };
45 reader.readAsText(file);
46 }
47 });
48});
49
50function formatFileSize(bytes) {
51 if (bytes < 1024) return bytes + " B";
52 if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
53 return (bytes / 1048576).toFixed(1) + " MB";
54}

best practice

Always check file types and sizes before processing. Set visual drop zone highlighting for dragenter/dragleave to give users clear feedback. For large files, consider streaming or worker-based processing. Show a preview for images before uploading.
Live Preview: Reorderable List

A practical drag-and-drop implementation for reordering list items. This pattern is used in kanban boards, playlists, and task management UIs.

preview
Touch Events Fallback

The HTML Drag and Drop API does not work on touch devices (iOS Safari, Android browsers). For mobile support, implement a touch event fallback using touchstart, touchmove, and touchend.

touch-fallback.js
JavaScript
1let touchDragged = null;
2let touchClone = null;
3let touchOffsetX = 0;
4let touchOffsetY = 0;
5
6document.addEventListener("touchstart", (e) => {
7 const item = e.target.closest("[draggable]");
8 if (!item) return;
9
10 touchDragged = item;
11 const touch = e.touches[0];
12 const rect = item.getBoundingClientRect();
13 touchOffsetX = touch.clientX - rect.left;
14 touchOffsetY = touch.clientY - rect.top;
15
16 // Create a visual clone for drag feedback
17 touchClone = item.cloneNode(true);
18 touchClone.style.cssText = `
19 position: fixed;
20 width: ${rect.width}px;
21 opacity: 0.8;
22 pointer-events: none;
23 z-index: 9999;
24 transform: rotate(-3deg) scale(1.05);
25 `;
26 touchClone.style.left = (touch.clientX - touchOffsetX) + "px";
27 touchClone.style.top = (touch.clientY - touchOffsetY) + "px";
28 document.body.appendChild(touchClone);
29
30 item.classList.add("dragging");
31}, { passive: true });
32
33document.addEventListener("touchmove", (e) => {
34 if (!touchDragged || !touchClone) return;
35 e.preventDefault();
36
37 const touch = e.touches[0];
38 touchClone.style.left = (touch.clientX - touchOffsetX) + "px";
39 touchClone.style.top = (touch.clientY - touchOffsetY) + "px";
40
41 // Hit test drop zones
42 const el = document.elementFromPoint(touch.clientX, touch.clientY);
43 const dropZone = el?.closest(".drop-zone");
44
45 document.querySelectorAll(".drop-zone").forEach((z) => {
46 z.classList.toggle("drag-over", z === dropZone);
47 });
48}, { passive: false });
49
50document.addEventListener("touchend", (e) => {
51 if (!touchDragged) return;
52
53 const touch = e.changedTouches[0];
54 const el = document.elementFromPoint(touch.clientX, touch.clientY);
55 const dropZone = el?.closest(".drop-zone");
56
57 if (dropZone) {
58 const data = touchDragged.textContent;
59 dropZone.textContent = "Dropped: " + data.trim();
60 }
61
62 touchDragged.classList.remove("dragging");
63 document.querySelectorAll(".drop-zone").forEach((z) => {
64 z.classList.remove("drag-over");
65 });
66
67 if (touchClone) {
68 document.body.removeChild(touchClone);
69 }
70
71 touchDragged = null;
72 touchClone = null;
73}, { passive: true });

warning

Touch events require {passive: false} on touchmove to call e.preventDefault(). Without this, the page scrolls while dragging. Use elementFromPoint for hit-testing drop targets since the drag API does not fire enter/leave events for touch.
Accessibility

Drag and drop is inherently inaccessible to keyboard and screen reader users. A keyboard alternative is essential for WCAG compliance. Implement move-up/move-down buttons or a dialog for position selection.

drag-drop-a11y.html
HTML
1<!-- Keyboard alternative for a reorderable list -->
2<ul class="sortable-list" role="listbox" aria-label="Task list">
3 <li role="option" draggable="true" tabindex="0" data-id="1">
4 <span class="item-text">Review pull request</span>
5 <div class="reorder-buttons">
6 <button type="button" class="move-up" aria-label="Move up"
7 onclick="moveItem(this, -1)">
8
9 </button>
10 <button type="button" class="move-down" aria-label="Move down"
11 onclick="moveItem(this, 1)">
12
13 </button>
14 </div>
15 </li>
16 <li role="option" draggable="true" tabindex="0" data-id="2">
17 <span class="item-text">Write unit tests</span>
18 <div class="reorder-buttons">
19 <button type="button" class="move-up" aria-label="Move up"
20 onclick="moveItem(this, -1)">
21
22 </button>
23 <button type="button" class="move-down" aria-label="Move down"
24 onclick="moveItem(this, 1)">
25
26 </button>
27 </div>
28 </li>
29</ul>
30
31<script>
32function moveItem(button, direction) {
33 const item = button.closest("li");
34 const list = item.parentElement;
35
36 if (direction === -1 && item.previousElementSibling) {
37 list.insertBefore(item, item.previousElementSibling);
38 item.querySelector(".move-up").focus();
39 } else if (direction === 1 && item.nextElementSibling) {
40 list.insertBefore(item.nextElementSibling, item);
41 item.querySelector(".move-down").focus();
42 }
43}
44</script>

best practice

Never rely solely on drag and drop for essential functionality. All reordering and movement operations must have keyboard-accessible alternatives. Use role="listbox" with role="option" for sortable lists, or implement a modal dialog that allows users to select the target position from a list.
Use Cases

Drag and drop is used across many application types. Here are the most common patterns:

Use CaseImplementationKey Considerations
File UploadDrop zone + FileReaderPreview, size limits, progress indicator
Reorderable ListsSortable list with insertBefore/afterTouch fallback, keyboard alternative
Kanban BoardCards between columnsColumn detection, card snap, persist order
Drag to SelectMulti-select by dragging over itemsLasso selection, shift-click integration
Drag to UploadDrag file from OS to browserMIME type filtering, chunked upload
Drag to BookmarkDrag link to bookmark barBrowser-native, limited customization
Best Practices
Always call e.preventDefault() in dragover to allow drop
Set effectAllowed on dragstart and dropEffect on dragover for correct cursor feedback
Use setData with multiple formats (text/plain fallback + custom type for structured data)
Provide visual feedback for all drag states: dragstart, dragover, and drop
Implement touch event fallbacks — the drag API does not work on mobile
Always provide keyboard alternatives for accessibility (move up/down buttons or position dialog)
Clean up drag styles and temporary elements in dragend or touchend
Check file types and sizes before processing dropped files
Use debounced handlers for drag events (they fire at high frequency)
Test across browsers — behavior differs for iframes, scrolling containers, and nested elements
Consider using a library (SortableJS, react-dnd, dnd-kit) for complex drag-and-drop UIs
Never prevent default drop if you don't intend to handle it — blocking native behavior breaks usability

best practice

For production drag-and-drop UIs, consider using a purpose-built library rather than the native API. Libraries like SortableJS, dnd-kit, or react-beautiful-dnd handle touch events, accessibility, complex sorting, and cross-browser edge cases. The native API is best for simple single-item drag operations and file drop zones.
$Blueprint — Engineering Documentation·Section ID: HTML-35·Revision: 1.0