🚀AdvancedAdvanced Forms
Drag & Drop File Upload
Build drag-and-drop file upload areas using the HTML Drag and Drop API handle dragover, drop events, and access files via DataTransfer.
Overview
The HTML Drag and Drop API enables users to drag files from their desktop directly into a web page drop zone. The API works through a sequence of drag events fired on the source (being dragged) and target (being dropped onto) elements. For file uploads, the files are available via the `event.dataTransfer.files` FileList on the `drop` event.
Building a good drop zone requires handling four events: `dragenter` and `dragover` (to signal the element accepts drops and prevent default browser behavior), `dragleave` (to remove visual feedback when leaving the zone), and `drop` (to capture the files). Calling `event.preventDefault()` on `dragover` is crucial without it, the browser navigates to the dropped file instead of handing it to your code.
Drag and drop and the traditional file input `<input type="file">` are complementary provide both for maximum accessibility.
Key Points
- •Must call `event.preventDefault()` on `dragover` to make the element a valid drop target
- •`event.dataTransfer.files` on the `drop` event is a `FileList` same API as input.files
- •`event.dataTransfer.types` on `dragenter`/`dragover` tells you what is being dragged (e.g., "Files")
- •`dragenter` and `dragleave` can fire multiple times due to child elements use a counter or `relatedTarget` check
- •The `draggable="true"` attribute makes any element draggable
- •`event.dataTransfer.effectAllowed` / `dropEffect` controls the cursor icon (copy, move, link)
- •Drag-and-drop is not accessible by keyboard always provide an alternative file input
Examples
Full drag-and-drop file upload zone
<div
id="drop-zone"
role="region"
aria-label="File drop zone"
>
<p>Drag files here or <label for="file-input">browse</label></p>
<input id="file-input" type="file" multiple style="display:none">
</div>
<ul id="file-list"></ul>
<style>
#drop-zone {
border: 2px dashed #94a3b8;
border-radius: 12px;
padding: 3rem;
text-align: center;
cursor: pointer;
transition: background 0.2s;
}
#drop-zone.over {
background: #eff6ff;
border-color: #3b82f6;
}
</style>
<script>
const zone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
let dragCount = 0;
zone.addEventListener('dragenter', (e) => {
e.preventDefault();
dragCount++;
zone.classList.add('over');
});
zone.addEventListener('dragleave', () => {
dragCount--;
if (dragCount === 0) zone.classList.remove('over');
});
zone.addEventListener('dragover', (e) => {
e.preventDefault(); // CRITICAL allows drop
e.dataTransfer.dropEffect = 'copy';
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
dragCount = 0;
zone.classList.remove('over');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
function handleFiles(files) {
const list = document.getElementById('file-list');
for (const file of files) {
const li = document.createElement('li');
li.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
list.appendChild(li);
}
}
</script>The dragCount counter handles the edge case where dragging over child elements causes spurious dragleave events.
Detecting what type of content is being dragged
<div id="smart-zone">Drop files or text here</div>
<script>
const zone = document.getElementById('smart-zone');
zone.addEventListener('dragover', (e) => {
e.preventDefault();
const types = e.dataTransfer.types;
if (types.includes('Files')) {
e.dataTransfer.dropEffect = 'copy';
zone.textContent = 'Drop files here';
} else if (types.includes('text/plain')) {
e.dataTransfer.dropEffect = 'move';
zone.textContent = 'Drop text here';
}
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
if (e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files);
} else {
const text = e.dataTransfer.getData('text/plain');
console.log('Dropped text:', text);
}
});
</script>Making an element draggable and setting transfer data
<ul id="sortable-list">
<li draggable="true" data-id="1">Item A</li>
<li draggable="true" data-id="2">Item B</li>
<li draggable="true" data-id="3">Item C</li>
</ul>
<script>
let draggedId;
document.querySelectorAll('[draggable]').forEach(el => {
el.addEventListener('dragstart', (e) => {
draggedId = el.dataset.id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedId);
el.classList.add('dragging');
});
el.addEventListener('dragend', () => el.classList.remove('dragging'));
el.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
el.addEventListener('drop', (e) => {
e.preventDefault();
// Reorder logic here using draggedId and el.dataset.id
});
});
</script>Common Mistakes
- Forgetting `event.preventDefault()` on `dragover` without it, the browser opens the file instead of firing the drop event
- Not accounting for child element `dragleave` events use a dragCounter or check `relatedTarget` to avoid false exits
- Building a drop-only UI without a keyboard-accessible fallback drag-and-drop is entirely mouse/touch only
Note
To restrict drop to specific file types, check `e.dataTransfer.items` on `dragover` (items expose MIME type without reading the file) and call `e.preventDefault()` conditionally to refuse wrong types.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Loading editor…
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4