Complete HTML Course Module 14: Advanced: HTML Web APIs

HTML Drag and Drop API

  • Beginner
  • Lesson 14.3
  • 6 min read
  • 60 of 69 in the course

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):

EventFires onWhen
dragstartThe dragged elementThe user starts dragging
dragThe dragged elementRepeatedly while dragging
dragenterThe drop targetThe dragged item enters it
dragoverThe drop targetRepeatedly while the item is over it
dragleaveThe drop targetThe item leaves it
dropThe drop targetThe user releases the mouse over it
dragendThe dragged elementThe 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, and forEach runs the same code for each one.
  • On dragstart, the card puts its id into dataTransfer and gets the dragging class, 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 the over class shows a dashed outline.
  • On dragleave, the outline is removed.
  • On drop, the column reads the id, finds the card with getElementById, and append moves 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() in dragover. The cursor shows a "not allowed" sign and drop never runs. Also call it in drop, 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 dragstart on the item and dragover plus drop on the target.
  • Call event.preventDefault() in dragover, or dropping is not allowed.
  • Use dataTransfer.setData() and getData() to pass the item's id; dropped files are in dataTransfer.files.
  • Always offer a keyboard alternative, such as a "Move to" menu or buttons.