Mastering the HTML5 Drag & Drop API: A Complete Developer Guide
Summary
draggable attribute, handling six core events (dragstart, drag, dragenter, dragover, dragleave, drop), and using the DataTransfer object to pass data between drag source and drop target. Perfect for building file uploaders, sortable lists, kanban boards, and interactive dashboards.Introduction
Understanding the basics
draggable="true" attribute.The six essential events
- dragstart - Fires when the user starts dragging an element
- drag - Continuously fires while the element is being dragged
- dragenter - Fires when a dragged element enters a valid drop target
- dragover - Continuously fires while a dragged element is over a drop target
- dragleave - Fires when a dragged element leaves a drop target
- drop - Fires when a dragged element is dropped on a valid drop target
Basic implementation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML5 Drag & Drop Demo</title>
<style>
.draggable {
padding: 20px;
margin: 10px;
background: #3498db;
color: white;
cursor: move;
border-radius: 5px;
display: inline-block;
}
.drop-zone {
min-height: 200px;
padding: 20px;
margin: 10px;
border: 3px dashed #95a5a6;
border-radius: 5px;
background: #ecf0f1;
}
.drop-zone.drag-over {
border-color: #3498db;
background: #d5e8f7;
}
</style>
</head>
<body>
<h1>Simple Drag & Drop Example</h1>
<div class="draggable" draggable="true" id="item1">
Drag me!
</div>
<div class="drop-zone" id="dropZone">
Drop here
</div>
<script>
const draggable = document.getElementById('item1');
const dropZone = document.getElementById('dropZone');
// Drag start event
draggable.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.id);
e.dataTransfer.effectAllowed = 'move';
});
// Prevent default to allow drop
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
// Visual feedback on drag enter
dropZone.addEventListener('dragenter', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
// Remove visual feedback on drag leave
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
// Handle the drop
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const id = e.dataTransfer.getData('text/plain');
const element = document.getElementById(id);
dropZone.appendChild(element);
});
</script>
</body>
</html>
The DataTransfer Object
DataTransfer object is the heart of the Drag & Drop API. It allows you to transfer data between the drag source and drop target.Key methods
Example with multiple data types
draggable.addEventListener('dragstart', (e) => {
// Store multiple data formats
e.dataTransfer.setData('text/plain', 'Simple text');
e.dataTransfer.setData('text/html', '<strong>HTML content</strong>');
e.dataTransfer.setData('application/json', JSON.stringify({
id: 'item1',
type: 'task',
priority: 'high'
}));
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
// Retrieve JSON data
const jsonData = e.dataTransfer.getData('application/json');
const data = JSON.parse(jsonData);
console.log('Dropped item:', data);
});
Advanced example: sortable list
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sortable List with Drag & Drop</title>
<style>
.sortable-list {
list-style: none;
padding: 0;
max-width: 400px;
margin: 20px auto;
}
.sortable-item {
padding: 15px 20px;
margin: 8px 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: move;
border-radius: 8px;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.sortable-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
.sortable-item.dragging {
opacity: 0.5;
}
.sortable-item.drag-over {
border-top: 3px solid #f39c12;
}
</style>
</head>
<body>
<h2 style="text-align: center;">Drag to Reorder Tasks</h2>
<ul class="sortable-list" id="taskList">
<li class="sortable-item" draggable="true">Design wireframes</li>
<li class="sortable-item" draggable="true">Develop frontend</li>
<li class="sortable-item" draggable="true">Backend integration</li>
<li class="sortable-item" draggable="true">Testing & QA</li>
<li class="sortable-item" draggable="true">Deploy to production</li>
</ul>
<script>
const list = document.getElementById('taskList');
let draggedElement = null;
// Add event listeners to all items
const items = list.querySelectorAll('.sortable-item');
items.forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
item.addEventListener('dragenter', handleDragEnter);
item.addEventListener('dragleave', handleDragLeave);
});
function handleDragStart(e) {
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
}
function handleDragEnd(e) {
this.classList.remove('dragging');
// Remove all drag-over classes
items.forEach(item => {
item.classList.remove('drag-over');
});
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDragEnter(e) {
if (this !== draggedElement) {
this.classList.add('drag-over');
}
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedElement !== this) {
// Swap elements
const allItems = Array.from(list.querySelectorAll('.sortable-item'));
const draggedIndex = allItems.indexOf(draggedElement);
const targetIndex = allItems.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentNode.insertBefore(draggedElement, this.nextSibling);
} else {
this.parentNode.insertBefore(draggedElement, this);
}
}
return false;
}
</script>
</body>
</html>
File upload with drag & drop
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File Upload with Drag & Drop</title>
<style>
.upload-zone {
width: 500px;
height: 300px;
margin: 50px auto;
border: 3px dashed #ccc;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f9f9f9;
transition: all 0.3s;
}
.upload-zone.active {
border-color: #4CAF50;
background: #e8f5e9;
}
.file-list {
margin-top: 20px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.file-item {
padding: 10px;
background: white;
margin: 5px 0;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="upload-zone" id="uploadZone">
<p> Drop files here or click to upload</p>
<input type="file" id="fileInput" multiple style="display: none;">
</div>
<div class="file-list" id="fileList"></div>
<script>
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop zone when dragging over it
['dragenter', 'dragover'].forEach(eventName => {
uploadZone.addEventListener(eventName, () => {
uploadZone.classList.add('active');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadZone.addEventListener(eventName, () => {
uploadZone.classList.remove('active');
}, false);
});
// Handle dropped files
uploadZone.addEventListener('drop', handleDrop, false);
// Click to upload
uploadZone.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
function handleFiles(files) {
fileList.innerHTML = '';
[...files].forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<strong>${file.name}</strong>
(${formatBytes(file.size)})
- ${file.type || 'Unknown type'}
`;
fileList.appendChild(fileItem);
});
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
</script>
</body>
</html>
Best practices and tips
dragover and drop events to allow dropping.drag and dragover events as they fire continuously.