Drag & Drop

Difficulty: Advanced

The HTML5 Drag and Drop API enables native drag-and-drop interactions in the browser without any third-party libraries. It works by marking elements as draggable, then listening for a series of events that fire during the drag lifecycle. While the API can feel verbose compared to library-based solutions, understanding it is essential for building custom interactive interfaces.

The drag lifecycle consists of several events. On the dragged element: dragstart fires when the user begins dragging (this is where you set the data payload), drag fires continuously while dragging, and dragend fires when the drag operation completes (whether it was dropped or cancelled). On the drop target: dragenter fires when the dragged element enters the target, dragover fires continuously while hovering over the target, dragleave fires when the dragged element leaves the target, and drop fires when the user releases the element over the target.

The DataTransfer object is the core of the drag-and-drop data model. In the dragstart handler, you call event.dataTransfer.setData(type, data) to attach data to the drag operation. Common types include 'text/plain' for simple text and 'application/json' for structured data. In the drop handler, you retrieve the data with event.dataTransfer.getData(type). You can also set the drag image, the allowed effect (copy, move, link), and the drop effect using the DataTransfer properties.

A critical requirement that trips up every beginner: you must call event.preventDefault() in the dragover handler to allow a drop. By default, elements do not accept drops. The default dragover behavior is to reject the drop operation. Without preventDefault(), the drop event will never fire. This single line of code is the most common source of "my drop handler isn't working" bugs.

The API also supports dragging files from the desktop into the browser. When a user drags a file onto a drop zone, the drop event's dataTransfer.files property contains the FileList. Combined with the FileReader API, this enables file upload interfaces, image preview dropzones, and CSV import features - all without a traditional file input.

Code examples

Basic Drag and Drop

<style>
  .draggable {
    padding: 12px 20px;
    background: #3b82f6;
    color: white;
    cursor: grab;
    display: inline-block;
    margin: 8px;
  }
  .dropzone {
    width: 300px;
    height: 150px;
    border: 2px dashed #94a3b8;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 16px;
  }
  .dropzone.over {
    border-color: #3b82f6;
    background: #eff6ff;
  }
</style>

<div class="draggable" draggable="true" id="item-1">Drag me!</div>

<div class="dropzone" id="drop-target">Drop here</div>

<script>
  const item = document.getElementById('item-1');
  const zone = document.getElementById('drop-target');

  item.addEventListener('dragstart', (e) => {
    e.dataTransfer.setData('text/plain', e.target.id);
    e.dataTransfer.effectAllowed = 'move';
  });

  zone.addEventListener('dragover', (e) => {
    e.preventDefault(); // REQUIRED to allow drop
    zone.classList.add('over');
  });

  zone.addEventListener('dragleave', () => {
    zone.classList.remove('over');
  });

  zone.addEventListener('drop', (e) => {
    e.preventDefault();
    zone.classList.remove('over');
    const id = e.dataTransfer.getData('text/plain');
    const element = document.getElementById(id);
    zone.textContent = '';
    zone.appendChild(element);
  });
</script>

The draggable attribute enables dragging. dragstart sets the data payload (the element's id). dragover must call preventDefault() or the drop will be rejected. The drop handler retrieves the data and moves the element.

Sortable List

<style>
  .sort-list { list-style: none; padding: 0; width: 250px; }
  .sort-list li {
    padding: 12px 16px;
    margin: 4px 0;
    background: #f1f5f9;
    border: 1px solid #e2e8f0;
    cursor: grab;
  }
  .sort-list li.dragging { opacity: 0.4; }
</style>

<ul class="sort-list" id="sortable">
  <li draggable="true">Item 1</li>
  <li draggable="true">Item 2</li>
  <li draggable="true">Item 3</li>
  <li draggable="true">Item 4</li>
</ul>

<script>
  const list = document.getElementById('sortable');
  let draggedItem = null;

  list.addEventListener('dragstart', (e) => {
    draggedItem = e.target;
    e.target.classList.add('dragging');
    e.dataTransfer.effectAllowed = 'move';
  });

  list.addEventListener('dragover', (e) => {
    e.preventDefault();
    const target = e.target.closest('li');
    if (target && target !== draggedItem) {
      const rect = target.getBoundingClientRect();
      const midY = rect.top + rect.height / 2;
      if (e.clientY < midY) {
        list.insertBefore(draggedItem, target);
      } else {
        list.insertBefore(draggedItem, target.nextSibling);
      }
    }
  });

  list.addEventListener('dragend', (e) => {
    e.target.classList.remove('dragging');
    draggedItem = null;
  });
</script>

This creates a reorderable list. During dragover, we calculate whether the cursor is above or below the midpoint of each item, then insert the dragged item before or after accordingly. The visual feedback (opacity) shows which item is being dragged.

File Drop Zone

<style>
  .file-drop {
    width: 400px;
    height: 200px;
    border: 2px dashed #94a3b8;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
  }
  .file-drop.over { border-color: #3b82f6; background: #eff6ff; }
</style>

<div class="file-drop" id="file-zone">
  <p>Drop files here</p>
</div>
<ul id="file-list"></ul>

<script>
  const zone = document.getElementById('file-zone');
  const fileList = document.getElementById('file-list');

  // Prevent default browser behavior (opening the file)
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
    zone.addEventListener(evt, (e) => e.preventDefault());
  });

  zone.addEventListener('dragenter', () => zone.classList.add('over'));
  zone.addEventListener('dragleave', () => zone.classList.remove('over'));

  zone.addEventListener('drop', (e) => {
    zone.classList.remove('over');
    const files = e.dataTransfer.files;

    Array.from(files).forEach((file) => {
      const li = document.createElement('li');
      const size = (file.size / 1024).toFixed(1);
      li.textContent = `${file.name} (${size} KB) - ${file.type}`;
      fileList.appendChild(li);
    });
  });
</script>

File drag-and-drop uses the same events but reads from dataTransfer.files instead of getData(). preventDefault() on all drag events prevents the browser from opening the file. The FileList contains File objects with name, size, and type properties.

Kanban Board Columns

<style>
  .board { display: flex; gap: 16px; }
  .column {
    width: 200px;
    min-height: 200px;
    background: #f8fafc;
    border: 1px solid #e2e8f0;
    padding: 12px;
  }
  .column h3 { margin: 0 0 12px; }
  .task {
    padding: 8px 12px;
    background: white;
    border: 1px solid #e2e8f0;
    margin-bottom: 8px;
    cursor: grab;
  }
  .column.over { background: #eff6ff; }
</style>

<div class="board">
  <div class="column" id="todo">
    <h3>To Do</h3>
    <div class="task" draggable="true" id="task-1">Design mockups</div>
    <div class="task" draggable="true" id="task-2">Write tests</div>
  </div>
  <div class="column" id="progress">
    <h3>In Progress</h3>
    <div class="task" draggable="true" id="task-3">Build API</div>
  </div>
  <div class="column" id="done">
    <h3>Done</h3>
  </div>
</div>

<script>
  document.querySelectorAll('.task').forEach(task => {
    task.addEventListener('dragstart', (e) => {
      e.dataTransfer.setData('text/plain', e.target.id);
    });
  });

  document.querySelectorAll('.column').forEach(col => {
    col.addEventListener('dragover', (e) => {
      e.preventDefault();
      col.classList.add('over');
    });
    col.addEventListener('dragleave', () => {
      col.classList.remove('over');
    });
    col.addEventListener('drop', (e) => {
      e.preventDefault();
      col.classList.remove('over');
      const id = e.dataTransfer.getData('text/plain');
      const task = document.getElementById(id);
      col.appendChild(task);
    });
  });
</script>

A simplified Kanban board with three columns. Tasks can be dragged between columns. Each column listens for dragover (with preventDefault) and drop events. The visual highlight (.over class) shows valid drop targets.

Key points

Concepts covered

draggable Attribute, dragstart, dragover, drop, DataTransfer, Sortable Lists