In short
The HTML Drag and Drop API lets users pick up elements with the mouse and drop them somewhere else, using the draggable attribute and a few drag events. You see drag and drop everywhere: sorting tasks in a to-do app, moving cards on a kanban board, or dropping files to upload them.
The HTML Drag and Drop API lets users pick up elements with the mouse and drop them somewhere else, using the draggable attribute and a few drag events.
You see drag and drop everywhere: sorting tasks in a to-do app, moving cards on a kanban board, or dropping files to upload them. In this lesson you will build a working kanban board and learn how to make it accessible for keyboard users too.
Making an element draggable
Images and links can be dragged by default. To make any other element draggable, add the draggable attribute:
<div class="card" draggable="true">Write blog post</div>
The value must be "true". Writing only draggable without a value does not work, because it is not a normal boolean attribute. Now the user can pick up the card and see a "ghost" copy following the mouse. But nothing can be dropped yet. For that, you need events.
The drag and drop events
During a drag, the browser fires several events (signals your JavaScript can listen to):
| Event | Fires on | When |
|---|---|---|
dragstart | The dragged element | The user starts dragging |
drag | The dragged element | Repeatedly while dragging |
dragenter | The drop target | The dragged item enters it |
dragover | The drop target | Repeatedly while the item is over it |
dragleave | The drop target | The item leaves it |
drop | The drop target | The user releases the mouse over it |
dragend | The dragged element | The drag finishes (dropped or cancelled) |
The most important rule: by default, elements do not accept drops. To allow a drop, you must call event.preventDefault() in the dragover event. If you forget this, the drop event never fires.
Passing data with dataTransfer
How does the drop target know what was dropped? Through event.dataTransfer, a small "envelope" that travels with the drag.
- In
dragstart, put something in the envelope:event.dataTransfer.setData("text/plain", card.id). - In
drop, read it back:event.dataTransfer.getData("text/plain").
The first argument is the type of data. "text/plain" is the simplest. Usually you store the id of the dragged element, then find the element by that id.
Drag and drop example: a kanban board
A kanban board has columns like "To do", "Doing" and "Done", with task cards you move between them. Save this as one HTML file and open it:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kanban Board</title>
<style>
.board { display: flex; gap: 1rem; }
.column { flex: 1; min-height: 200px; padding: 0.5rem; background: #f1f3f5; border-radius: 8px; }
.column.over { outline: 3px dashed #4c6ef5; }
.card { padding: 0.75rem; margin-bottom: 0.5rem; background: white; border-radius: 6px; cursor: grab; }
.card.dragging { opacity: 0.4; }
</style>
</head>
<body>
<h1>Website launch</h1>
<div class="board">
<section class="column" aria-labelledby="todo-title">
<h2 id="todo-title">To do</h2>
<div class="card" id="card-1" draggable="true">Write the About page</div>
<div class="card" id="card-2" draggable="true">Choose a domain name</div>
</section>
<section class="column" aria-labelledby="doing-title">
<h2 id="doing-title">Doing</h2>
<div class="card" id="card-3" draggable="true">Design the logo</div>
</section>
<section class="column" aria-labelledby="done-title">
<h2 id="done-title">Done</h2>
</section>
</div>
<script>
const cards = document.querySelectorAll(".card");
const columns = document.querySelectorAll(".column");
cards.forEach((card) => {
card.addEventListener("dragstart", (event) => {
event.dataTransfer.setData("text/plain", card.id);
card.classList.add("dragging");
});
card.addEventListener("dragend", () => {
card.classList.remove("dragging");
});
});
columns.forEach((column) => {
column.addEventListener("dragover", (event) => {
event.preventDefault();
column.classList.add("over");
});
column.addEventListener("dragleave", () => {
column.classList.remove("over");
});
column.addEventListener("drop", (event) => {
event.preventDefault();
const id = event.dataTransfer.getData("text/plain");
column.append(document.getElementById(id));
column.classList.remove("over");
});
});
</script>
</body>
</html>
Let's walk through the script:
querySelectorAll(".card")gets all cards, andforEachruns the same code for each one.- On
dragstart, the card puts itsidintodataTransferand gets thedraggingclass, which makes it half see-through. - On
dragend, the class is removed, whether the drop worked or not. - Each column listens for
dragover.event.preventDefault()says "yes, you may drop here", and theoverclass shows a dashed outline. - On
dragleave, the outline is removed. - On
drop, the column reads the id, finds the card withgetElementById, andappendmoves it to the end of the column.
Drag "Design the logo" into "Done". The column shows a blue dashed outline while you hover, and the card moves when you let go.
Common mistake: Forgetting
event.preventDefault()indragover. The cursor shows a "not allowed" sign anddropnever runs. Also call it indrop, because for some content (like links or files) the browser's default is to open the dropped item.
Dropping files from the computer
The same events work for files dragged from the desktop. The files are in event.dataTransfer.files:
<div id="drop-zone" style="padding: 2rem; border: 3px dashed #868e96;">Drop images here</div>
<ul id="file-list"></ul>
<script>
const zone = document.querySelector("#drop-zone");
const list = document.querySelector("#file-list");
zone.addEventListener("dragover", (event) => event.preventDefault());
zone.addEventListener("drop", (event) => {
event.preventDefault();
for (const file of event.dataTransfer.files) {
const item = document.createElement("li");
item.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
list.append(item);
}
});
</script>
Drop a few files on the box and you will see each file name and size in a list. for (const file of ...) loops over every dropped file. To actually upload them, you would send them to the server with fetch(), which you will learn in the JavaScript course. Always keep a normal <input type="file"> too, for people who can't drag.
Accessibility: always add a keyboard alternative
The Drag and Drop API only works with a mouse (and, in many browsers, touch). Keyboard and screen reader users cannot drag at all. So every drag action needs another way to do the same thing. A simple solution is a "Move to" menu on each card:
<div class="card" id="card-1" draggable="true">
Write the About page
<label for="move-1" class="visually-hidden">Move to</label>
<select id="move-1" class="move">
<option value="">Move to…</option>
<option value="todo-title">To do</option>
<option value="doing-title">Doing</option>
<option value="done-title">Done</option>
</select>
</div>
<script>
document.querySelectorAll(".move").forEach((menu) => {
menu.addEventListener("change", () => {
const column = document.getElementById(menu.value).parentElement;
column.append(menu.closest(".card"));
menu.focus();
});
});
</script>
When the user picks a column, menu.value is the id of that column's heading, .parentElement gets the column around it, and menu.closest(".card") finds the card the menu belongs to. The card moves, and focus() keeps the keyboard focus on the menu so the user doesn't get lost. The visually-hidden class is a common CSS class that hides the label visually while screen readers still read it.
Note: Building fully sortable lists (dropping between two cards) needs more code to work out the mouse position. Many teams use a well-tested library such as SortableJS for this, which also helps with touch screens.
Practice exercise
Add a fourth column called "Blocked" to the kanban board, and two more cards. Then show the number of cards in each column heading, like "Done (1)", and update it after every drop.
Hint: After moving a card, count the cards in a column with column.querySelectorAll(".card").length and update the heading's textContent. You will learn how to save the board in the next lesson.
Key takeaways
- Add
draggable="true"to make an element draggable; images and links are draggable by default. - Listen for
dragstarton the item anddragoverplusdropon the target. - Call
event.preventDefault()indragover, or dropping is not allowed. - Use
dataTransfer.setData()andgetData()to pass the item's id; dropped files are indataTransfer.files. - Always offer a keyboard alternative, such as a "Move to" menu or buttons.