HTML5 APIs
Drag & Drop API
Build interactive drag-and-drop interfaces with the HTML5 Drag and Drop API. Learn how to create file uploaders, sortable lists, and draggable UI elements.
What is the Drag & Drop API?
The Drag and Drop API allows users to drag elements and drop them onto other elements, enabling intuitive interactions like file uploads, reordering lists, and moving items between containers.
🎯 Native Support
Built into HTML5, no libraries needed
📠File Drag
Users can drag files from desktop
🔄 Reordering
Sortable lists and kanban boards
🎨 Customizable
Full control over drag feedback
Basic Drag & Drop
Making Elements Draggable
HTML
<!DOCTYPE html>
<html>
<head>
<style>
.draggable {
padding: 20px;
background: #4CAF50;
color: white;
cursor: move;
margin: 10px;
}
.dropzone {
min-height: 200px;
padding: 20px;
border: 3px dashed #ccc;
margin: 10px;
}
.dropzone.dragover {
background: #e8f5e9;
border-color: #4CAF50;
}
</style>
</head>
<body>
<!-- Draggable element -->
<div class="draggable" draggable="true" ondragstart="handleDragStart(event)">
Drag me!
</div>
<!-- Drop zone -->
<div class="dropzone"
ondrop="handleDrop(event)"
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)">
Drop here
</div>
<script>
function handleDragStart(e) {
// Store dragged element's data
e.dataTransfer.setData('text/plain', e.target.textContent);
e.dataTransfer.effectAllowed = 'move';
}
function handleDragOver(e) {
e.preventDefault(); // Allow drop
e.currentTarget.classList.add('dragover');
e.dataTransfer.dropEffect = 'move';
}
function handleDragLeave(e) {
e.currentTarget.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
// Get dragged data
const data = e.dataTransfer.getData('text/plain');
e.currentTarget.textContent = 'Dropped: ' + data;
}
</script>
</body>
</html>Drag Events
Events on Draggable Element
dragstart- User starts draggingdrag- Element is being dragged (fires continuously)dragend- Drag operation ends
Events on Drop Target
dragenter- Dragged element enters drop zonedragover- Dragged element is over drop zone (fires continuously)dragleave- Dragged element leaves drop zonedrop- Element is dropped
JAVASCRIPT
const draggable = document.getElementById('draggable');
const dropzone = document.getElementById('dropzone');
// Draggable element events
draggable.addEventListener('dragstart', (e) => {
console.log('Drag started');
e.dataTransfer.setData('text/plain', 'data');
});
draggable.addEventListener('drag', (e) => {
// Fires continuously while dragging
});
draggable.addEventListener('dragend', (e) => {
console.log('Drag ended');
});
// Drop zone events
dropzone.addEventListener('dragenter', (e) => {
console.log('Entered drop zone');
e.currentTarget.classList.add('drag-enter');
});
dropzone.addEventListener('dragover', (e) => {
e.preventDefault(); // REQUIRED to allow drop!
});
dropzone.addEventListener('dragleave', (e) => {
console.log('Left drop zone');
e.currentTarget.classList.remove('drag-enter');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
console.log('Dropped!');
const data = e.dataTransfer.getData('text/plain');
});dataTransfer Object
Setting Data
JAVASCRIPT
element.addEventListener('dragstart', (e) => {
// Store different data types
e.dataTransfer.setData('text/plain', 'Simple text');
e.dataTransfer.setData('text/html', '<strong>HTML</strong>');
e.dataTransfer.setData('application/json', JSON.stringify({ id: 123 }));
// Set drag effect
e.dataTransfer.effectAllowed = 'move'; // or 'copy', 'link', 'copyMove', 'all'
});Getting Data
JAVASCRIPT
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
// Get data (must match MIME type used in setData)
const text = e.dataTransfer.getData('text/plain');
const html = e.dataTransfer.getData('text/html');
const json = JSON.parse(e.dataTransfer.getData('application/json'));
console.log('Dropped:', text);
});Setting Drag Image
JAVASCRIPT
element.addEventListener('dragstart', (e) => {
// Use custom drag image
const dragImage = document.createElement('div');
dragImage.textContent = '📦 Dragging...';
dragImage.style.position = 'absolute';
dragImage.style.top = '-9999px';
document.body.appendChild(dragImage);
e.dataTransfer.setDragImage(dragImage, 0, 0);
// Clean up after drag
setTimeout(() => document.body.removeChild(dragImage), 0);
});Example: Sortable List
HTML
<!DOCTYPE html>
<html>
<head>
<style>
.sortable-list { list-style: none; padding: 0; }
.sortable-item {
padding: 15px;
margin: 5px 0;
background: #f5f5f5;
border: 2px solid #ddd;
cursor: move;
user-select: none;
}
.sortable-item.dragging {
opacity: 0.5;
}
.sortable-item.drag-over {
border-top: 3px solid #4CAF50;
}
</style>
</head>
<body>
<h2>Sortable List (Drag to Reorder)</h2>
<ul class="sortable-list" id="list">
<li class="sortable-item" draggable="true">Item 1</li>
<li class="sortable-item" draggable="true">Item 2</li>
<li class="sortable-item" draggable="true">Item 3</li>
<li class="sortable-item" draggable="true">Item 4</li>
</ul>
<script>
let draggedElement = null;
const items = document.querySelectorAll('.sortable-item');
items.forEach(item => {
item.addEventListener('dragstart', function(e) {
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
item.addEventListener('dragend', function() {
this.classList.remove('dragging');
// Remove all drag-over classes
items.forEach(item => item.classList.remove('drag-over'));
});
item.addEventListener('dragover', function(e) {
e.preventDefault();
if (this === draggedElement) return;
// Get mouse position relative to item
const rect = this.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
// Insert before or after based on position
if (e.clientY < midpoint) {
this.parentNode.insertBefore(draggedElement, this);
} else {
this.parentNode.insertBefore(draggedElement, this.nextSibling);
}
});
item.addEventListener('dragenter', function(e) {
if (this !== draggedElement) {
this.classList.add('drag-over');
}
});
item.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
});
</script>
</body>
</html>Example: File Drop Upload
HTML
<!DOCTYPE html>
<html>
<head>
<style>
.drop-zone {
width: 400px;
height: 200px;
border: 3px dashed #ccc;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #666;
transition: all 0.3s;
}
.drop-zone.dragover {
background: #e3f2fd;
border-color: #2196F3;
color: #2196F3;
}
.file-list {
margin-top: 20px;
}
.file-item {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-left: 4px solid #4CAF50;
}
</style>
</head>
<body>
<div class="drop-zone" id="dropZone">
Drop files here or click to upload
</div>
<div class="file-list" id="fileList"></div>
<script>
const dropZone = document.getElementById('dropZone');
const fileList = document.getElementById('fileList');
// Prevent default drag behaviors on document
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop zone when dragging
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.add('dragover');
});
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.remove('dragover');
});
});
// Handle dropped files
dropZone.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
handleFiles(files);
});
// Also allow click to upload
dropZone.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.onchange = (e) => handleFiles(e.target.files);
input.click();
});
function handleFiles(files) {
fileList.innerHTML = '';
[...files].forEach(file => {
const div = document.createElement('div');
div.className = 'file-item';
div.innerHTML = `
<strong>${file.name}</strong>
<br>
Size: ${formatFileSize(file.size)}
<br>
Type: ${file.type || 'unknown'}
`;
fileList.appendChild(div);
// Read file content (for images)
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
img.style.maxWidth = '200px';
img.style.marginTop = '10px';
div.appendChild(img);
};
reader.readAsDataURL(file);
}
});
}
function formatFileSize(bytes) {
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 Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
</script>
</body>
</html>Example: Kanban Board
HTML
<!DOCTYPE html>
<html>
<head>
<style>
.board {
display: flex;
gap: 20px;
padding: 20px;
}
.column {
flex: 1;
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
min-height: 400px;
}
.column h3 {
margin-top: 0;
color: #666;
}
.card {
background: white;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: move;
}
.card.dragging {
opacity: 0.5;
}
.column.drag-over {
background: #e8f5e9;
border: 2px dashed #4CAF50;
}
</style>
</head>
<body>
<div class="board">
<div class="column" data-status="todo">
<h3>To Do</h3>
<div class="card" draggable="true">Task 1</div>
<div class="card" draggable="true">Task 2</div>
</div>
<div class="column" data-status="inprogress">
<h3>In Progress</h3>
<div class="card" draggable="true">Task 3</div>
</div>
<div class="column" data-status="done">
<h3>Done</h3>
<div class="card" draggable="true">Task 4</div>
</div>
</div>
<script>
let draggedCard = null;
// Make cards draggable
function setupCard(card) {
card.addEventListener('dragstart', function(e) {
draggedCard = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
card.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
}
// Make columns drop targets
function setupColumn(column) {
column.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
column.addEventListener('dragleave', function(e) {
// Only remove if leaving column entirely
if (e.target === this) {
this.classList.remove('drag-over');
}
});
column.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (draggedCard) {
this.appendChild(draggedCard);
// Log status change
const newStatus = this.dataset.status;
console.log('Card moved to:', newStatus);
}
});
}
// Initialize
document.querySelectorAll('.card').forEach(setupCard);
document.querySelectorAll('.column').forEach(setupColumn);
</script>
</body>
</html>Mobile Touch Support
Native Drag & Drop API doesn't work on mobile. For mobile support, you need to handle touch events:
JAVASCRIPT
// Simple touch drag support
let touchDragging = false;
let touchElement = null;
element.addEventListener('touchstart', (e) => {
touchDragging = true;
touchElement = e.target;
// Add visual feedback
touchElement.classList.add('dragging');
});
element.addEventListener('touchmove', (e) => {
if (!touchDragging) return;
e.preventDefault();
// Move element with touch
const touch = e.touches[0];
touchElement.style.position = 'absolute';
touchElement.style.left = touch.pageX + 'px';
touchElement.style.top = touch.pageY + 'px';
});
element.addEventListener('touchend', (e) => {
touchDragging = false;
touchElement.classList.remove('dragging');
// Check drop target
const dropTarget = document.elementFromPoint(
e.changedTouches[0].clientX,
e.changedTouches[0].clientY
);
// Handle drop
if (dropTarget && dropTarget.classList.contains('dropzone')) {
dropTarget.appendChild(touchElement);
}
});
// Note: For production, consider using libraries like:
// - SortableJS (touch support included)
// - react-beautiful-dnd
// - interact.jsBest Practices
✅ Do
- Always call
e.preventDefault()in dragover - Provide visual feedback during drag
- Use appropriate cursor (cursor: move)
- Handle dragend to clean up state
- Test keyboard accessibility
- Consider mobile/touch support
- Add ARIA attributes for screen readers
⌠Don't
- Forget preventDefault() in dragover
- Make everything draggable (confusing UX)
- Ignore mobile users
- Forget to clean up event listeners
- Assume drag & drop is intuitive (provide hints)
Browser Support
Drag & Drop API is supported in all modern browsers:
- ✅ Chrome 4+
- ✅ Firefox 3.5+
- ✅ Safari 3.1+
- ✅ Edge (all versions)
- âš ï¸ Mobile: Limited native support (use touch events or libraries)