HTML Drag & Drop API
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.
The draggable attribute makes an HTML element draggable. By default, only images, links, and selected text are draggable. All other elements require draggable="true".
| Value | Effect |
|---|---|
| true | Element is draggable |
| false | Element is not draggable |
| auto | Browser default behavior (default) |
| 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
The drag lifecycle consists of seven events. Some fire on the drag source (draggable element), others on the drop target, and some on both.
| Event | Fires On | Description |
|---|---|---|
| dragstart | Source | Drag operation begins — set data and drag image |
| drag | Source | Continuously fires while dragging |
| dragend | Source | Drag operation ends (drop or cancel) |
| dragenter | Target | Dragged element enters a valid target |
| dragover | Target | Continuously fires while over target (must prevent default) |
| dragleave | Target | Dragged element leaves the target |
| drop | Target | Element is dropped on the target — receive data |
| 1 | // === Source element events === |
| 2 | |
| 3 | let draggedItem = null; |
| 4 | |
| 5 | document.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 | |
| 15 | document.addEventListener("drag", (e) => { |
| 16 | // Fires many times — avoid heavy computations here |
| 17 | }); |
| 18 | |
| 19 | document.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 | |
| 27 | document.addEventListener("dragenter", (e) => { |
| 28 | const target = e.target.closest(".drop-zone"); |
| 29 | if (!target) return; |
| 30 | target.classList.add("drag-over"); |
| 31 | }); |
| 32 | |
| 33 | document.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 | |
| 40 | document.addEventListener("dragleave", (e) => { |
| 41 | const target = e.target.closest(".drop-zone"); |
| 42 | if (!target) return; |
| 43 | target.classList.remove("drag-over"); |
| 44 | }); |
| 45 | |
| 46 | document.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 DataTransfer object is the bridge between drag source and drop target. It carries data, files, and drag metadata throughout the drag session.
| Method / Property | Description |
|---|---|
| setData(format, data) | Sets drag data for a specific MIME type or custom format |
| getData(format) | Retrieves drag data for a given format |
| types | Array of data formats set on the drag |
| files | FileList of dropped files (from OS) |
| effectAllowed | Allowed operations: none, copy, move, link, all |
| dropEffect | Current 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 |
| 1 | // Setting multiple data formats |
| 2 | element.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 |
| 14 | target.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 | |
| 35 | target.addEventListener("dragover", (e) => { |
| 36 | e.preventDefault(); |
| 37 | e.dataTransfer.dropEffect = "copy"; // shows "+" cursor |
| 38 | }); |
pro tip
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.
| 1 | // Custom drag image from an element |
| 2 | element.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 |
| 24 | const style = document.createElement("style"); |
| 25 | style.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 | `; |
| 38 | document.head.appendChild(style); |
info
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.
| 1 | const 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 | |
| 11 | dropZone.addEventListener("dragenter", () => { |
| 12 | dropZone.classList.add("highlight"); |
| 13 | }); |
| 14 | |
| 15 | dropZone.addEventListener("dragleave", () => { |
| 16 | dropZone.classList.remove("highlight"); |
| 17 | }); |
| 18 | |
| 19 | dropZone.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 | |
| 50 | function 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
A practical drag-and-drop implementation for reordering list items. This pattern is used in kanban boards, playlists, and task management UIs.
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.
| 1 | let touchDragged = null; |
| 2 | let touchClone = null; |
| 3 | let touchOffsetX = 0; |
| 4 | let touchOffsetY = 0; |
| 5 | |
| 6 | document.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 | |
| 33 | document.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 | |
| 50 | document.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
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.
| 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> |
| 32 | function 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
Drag and drop is used across many application types. Here are the most common patterns:
| Use Case | Implementation | Key Considerations |
|---|---|---|
| File Upload | Drop zone + FileReader | Preview, size limits, progress indicator |
| Reorderable Lists | Sortable list with insertBefore/after | Touch fallback, keyboard alternative |
| Kanban Board | Cards between columns | Column detection, card snap, persist order |
| Drag to Select | Multi-select by dragging over items | Lasso selection, shift-click integration |
| Drag to Upload | Drag file from OS to browser | MIME type filtering, chunked upload |
| Drag to Bookmark | Drag link to bookmark bar | Browser-native, limited customization |
best practice