🎯 Drag and Drop API
Grab something, move it, let go — it's the most natural interaction there is. The HTML5 Drag and Drop API brings that directness to the browser: sortable lists, kanban boards, and "drop files here" upload zones all rest on the same handful of events. This lesson demystifies them.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Make elements draggable and trace the seven drag events from start to end
- Create valid drop zones — and explain why
preventDefaultis mandatory - Move data with the dataTransfer object and choose the right effect
- Accept files dragged from the desktop and read their metadata
- Build an accessible sortable list and know when a library is the better call
Estimated Time: 40–50 minutes • Difficulty: Intermediate
Hands-on: Build a two-column drag-and-drop board that moves items between lists.
In This Lesson
Direct Manipulation on the Web
The HTML5 Drag and Drop (DnD) API lets users grab an on-screen element and move it somewhere else, mirroring how we shuffle papers on a desk. Because the gesture matches a physical intuition, drag-and-drop interfaces feel obvious with almost no instruction.
You'll find it powering:
- File uploads — dragging files from the desktop into the page
- Kanban boards — moving cards between "To Do" and "Done"
- Sortable lists — reordering items by priority
- Builders and editors — arranging blocks, images, or form fields
⚠️ One big caveat up front
The native DnD API does not fire on touch screens the way it does with a mouse. For phone and tablet support you'll either add Pointer Events yourself or use a library (covered at the end). Keep that in mind before shipping DnD as your only way to do something.
The Seven Drag Events
A drag is a sequence of events split between the source (what you're dragging) and the target (where you might drop it):
dragover and drop.Making Elements Draggable
Any element becomes draggable with one attribute:
<div id="card" draggable="true">Drag me!</div>
Images, links, and selected text are draggable by default. To make the drag mean something, handle dragstart — that's where you stash the data being moved and set the allowed effect:
const card = document.getElementById('card');
card.addEventListener('dragstart', (event) => {
// Attach the payload — here, the element's id
event.dataTransfer.setData('text/plain', event.target.id);
event.dataTransfer.effectAllowed = 'move';
card.classList.add('dragging'); // for CSS feedback
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging'); // always clean up
});
💡 Style the drag state in CSS
Add a class in dragstart and remove it in dragend, then let CSS do the visuals — e.g. .dragging { opacity: .5; }. Doing feedback in CSS instead of JavaScript keeps the drag smooth.
Creating Drop Zones
Here's the part that surprises everyone: by default the browser refuses drops. To turn an element into a valid drop zone you must call event.preventDefault() in its dragover handler. Skip that one line and drop never fires.
const zone = document.getElementById('drop-zone');
// preventDefault here is what makes dropping possible
zone.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
zone.classList.add('drag-over');
});
zone.addEventListener('dragleave', () => {
zone.classList.remove('drag-over');
});
zone.addEventListener('drop', (event) => {
event.preventDefault(); // stop the browser's default (e.g. opening a link)
zone.classList.remove('drag-over');
const id = event.dataTransfer.getData('text/plain');
const dragged = document.getElementById(id);
zone.appendChild(dragged); // move the element into the zone
});
💡 Why the double preventDefault? Think of the browser as having a "No Entry" sign on every element.preventDefaultindragoverremoves the sign so a drop is allowed;preventDefaultindropstops the browser's built-in behaviour (like navigating to a dropped link) so your code runs instead.
The dataTransfer Object
Every drag event carries a dataTransfer object — the courier that holds the payload and controls the visual effect.
| Member | What it does |
|---|---|
setData(format, data) | Store the payload under a MIME type |
getData(format) | Read it back in the drop handler |
setDragImage(el, x, y) | Use a custom image as the drag preview |
effectAllowed | Set in dragstart — which effects are permitted |
dropEffect | Set in dragover — the cursor shown (copy / move / link) |
files | A FileList when the user drops files from the OS |
You can store several formats at once, letting different targets read what they understand:
event.dataTransfer.setData('text/plain', 'Buy milk');
event.dataTransfer.setData('application/json',
JSON.stringify({ id: 123, type: 'task' }));
📖 effectAllowed vs dropEffect
effectAllowed (source, in dragstart) declares the menu of options — 'copy', 'move', 'copyMove', etc. dropEffect (target, in dragover) picks the single one currently in play, which decides the cursor icon the user sees.
Dropping Files
The killer feature: users can drag files straight from their file manager into your page. Dropped files land in event.dataTransfer.files as a FileList.
const zone = document.getElementById('file-zone');
// Prevent the browser from just opening the file
['dragover', 'drop'].forEach((name) =>
zone.addEventListener(name, (e) => e.preventDefault()));
zone.addEventListener('drop', (event) => {
const files = [...event.dataTransfer.files]; // FileList -> Array
for (const file of files) {
console.log(`${file.name} — ${formatSize(file.size)} — ${file.type}`);
// In a real app: upload with FormData + fetch
// const body = new FormData();
// body.append('file', file);
// await fetch('/upload', { method: 'POST', body });
}
});
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
}
✅ Always pair it with a normal file input
Drag-to-upload is a delight, but keep a plain <input type="file"> too — it's the accessible, touch-friendly, keyboard-friendly path. Drag and drop is the enhancement, not the only door in.
Hands-on: Two-Column Board
🏋️ Move tasks between "To Do" and "Done"
Objective: Build two columns and let the user drag task cards from one to the other — the foundation of every kanban board.
Starter HTML
<div class="board">
<ul class="column" id="todo">
<li class="task" draggable="true" id="t1">Write tests</li>
<li class="task" draggable="true" id="t2">Fix login bug</li>
</ul>
<ul class="column" id="done"></ul>
</div>
Your tasks
- On each task's
dragstart, store its id indataTransfer. - On each column's
dragover, callpreventDefault()so drops are allowed. - On
drop, read the id andappendChildthe task into that column. - Bonus: add a
.drag-overhighlight class on the column while an item hovers.
💡 Hint
Attach the task listeners with a loop over document.querySelectorAll('.task'), and the column listeners over document.querySelectorAll('.column'). In drop, event.currentTarget is the column you dropped onto.
✅ Solution
document.querySelectorAll('.task').forEach((task) => {
task.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', task.id);
e.dataTransfer.effectAllowed = 'move';
task.classList.add('dragging');
});
task.addEventListener('dragend', () => task.classList.remove('dragging'));
});
document.querySelectorAll('.column').forEach((col) => {
col.addEventListener('dragover', (e) => {
e.preventDefault(); // enable dropping
col.classList.add('drag-over');
});
col.addEventListener('dragleave', () => col.classList.remove('drag-over'));
col.addEventListener('drop', (e) => {
e.preventDefault();
col.classList.remove('drag-over');
const id = e.dataTransfer.getData('text/plain');
col.appendChild(document.getElementById(id));
});
});
That's a working board in ~20 lines. Persisting the layout is just a matter of saving each column's task ids to localStorage on every drop — tying this lesson back to the Web Storage one.
Accessibility & Libraries
Drag and drop is mouse-shaped by nature, which makes it a common accessibility trap. A keyboard-only or touch user must be able to accomplish the same thing another way.
✅ Do
- Provide a keyboard alternative — e.g. "Move up / Move down" buttons on each item.
- Show clear visual feedback: highlight valid drop zones and the item being dragged.
- Announce changes to screen readers with an
aria-liveregion. - Always keep a non-drag path (buttons, a file input) to the same outcome.
🚫 Don't
- Make drag the only way to perform an action.
- Forget to remove drag styling in
dragend— orphaned classes linger. - Do heavy work in
dragover; it fires many times per second. - Assume it works on touch — test on real devices.
When to reach for a library
The native API is fine for simple cases, but production apps often want touch support, smooth animations, and accessibility baked in. Popular choices:
| Library | Best for |
|---|---|
| SortableJS | Framework-agnostic sortable lists with touch support |
| @dnd-kit | Modern, accessible drag and drop for React |
| interact.js | Dragging, resizing, snapping, and gestures |
With SortableJS the earlier board becomes almost nothing:
import Sortable from 'sortablejs';
new Sortable(document.getElementById('todo'), {
group: 'board', // shared group lets items move between lists
animation: 150,
onEnd: (evt) => console.log('moved to index', evt.newIndex)
});
Summary & Quiz
🎉 Key Takeaways
- Set
draggable="true", then handle dragstart (store data) and dragend (clean up) on the source. - A drop zone needs
preventDefault()in both dragover and drop — without it, nothing drops. - The dataTransfer object carries the payload and controls the copy/move/link effect.
- Dropped files arrive in
dataTransfer.filesas a FileList. - Native DnD isn't touch- or keyboard-friendly — always provide an alternative, or use a library.
🎯 Quick Quiz
Question 1: Your drop handler never runs, no matter where you release the item. What's the most likely cause?
Question 2: Where do files land when a user drags them from their desktop onto a drop zone?
Question 3: Which is the strongest accessibility practice for a drag-and-drop feature?
📚 Further Reading
🚀 What's Next?
You've now handled pointers, files, and direct manipulation. Next we shift from moving elements to drawing them: the fundamentals of SVG, where shapes are described in markup and scale crisply to any size.
🎉 Nice work!
Sortable lists, kanban boards, and drop-to-upload are all within reach now — just remember the alternative path.