The HTML5 Drag and Drop API

This post explains the basics of drag and drop.

Create draggable content

In most browsers, text selections, images, and links are draggable by default. For example, if you drag a link on a web page you'ill see a small box with a title and URL that you can drop on the address bar or the desktop to create a shortcut or navigate to the link. To make other types of content draggable, you need to use the HTML5 Drag and Drop APIs.

To make an object draggable, set draggable=true on that element. Just about anything can be drag-enabled, including images, files, links, files, or any markup on your page.

The following example creates an interface to rearrange columns that have been laid out with CSS Grid. The basic markup for the columns looks like this, with the draggable attribute for each column set to true:

<div class="container">
  <div draggable="true" class="box">A</div>
  <div draggable="true" class="box">B</div>
  <div draggable="true" class="box">C</div>
</div>

Here's the CSS for the container and box elements. The only CSS related to the drag feature is the cursor: move property. The rest of the code controls the layout and styling of the container and box elements.

.container {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
}

.box {
  border: 3px solid #666;
  background-color: #ddd;
  border-radius: .5em;
  padding: 10px;
  cursor: move;
}

At this point you can drag the items, but nothing else happens. To add behavior, you need to use the JavaScript API.

Listen for dragging events

To monitor the drag process, you can listen for any of the following events:

To handle the drag flow, you need some kind of source element (where the drag starts), the data payload (the thing being dragged), and a target (an area to catch the drop). The source element can be almost any kind of element. The target is the drop zone or set of drop zones that accepts the data the user is trying to drop. Not all elements can be targets. For example, your target can't be an image.

Start and end a drag sequence

After you define draggable="true" attributes on your content, attach a dragstart event handler to start the drag sequence for each column.

This code sets the column's opacity to 40% when the user starts dragging it, then return it to 100% when the dragging event ends.

function handleDragStart(e) {
  this.style.opacity = '0.4';
}

function handleDragEnd(e) {
  this.style.opacity = '1';
}

let items = document.querySelectorAll('.container .box');
items.forEach(function (item) {
  item.addEventListener('dragstart', handleDragStart);
  item.addEventListener('dragend', handleDragEnd);
});

The result can be seen in the following Glitch demo. Drag an item, and its opacity changes. Because the source element has the dragstart event, setting this.style.opacity to 40% gives the user visual feedback that that element is the current selection being moved. When you drop the item, the source element returns to 100% opacity, even though you haven't defined the drop behavior yet.

Add additional visual cues

To help the user understand how to interact with your interface, use the dragenter, dragover and dragleave event handlers. In this example, the columns are drop targets in addition to being draggable. Help the user to understand this by making the border dashed when they hold a dragged item over a column. For example, in your CSS, you might create an over class for elements that are drop targets:

.box.over {
  border: 3px dotted #666;
}

Then, in your JavaScript, set up the event handlers, add the over class when the column is dragged over, and remove it when the dragged element leaves. In the dragend handler we also make sure to remove the classes at the end of the drag.

document.addEventListener('DOMContentLoaded', (event) => {

  function handleDragStart(e) {
    this.style.opacity = '0.4';
  }

  function handleDragEnd(e) {
    this.style.opacity = '1';

    items.forEach(function (item) {
      item.classList.remove('over');
    });
  }

  function handleDragOver(e) {
    e.preventDefault();
    return false;
  }

  function handleDragEnter(e) {
    this.classList.add('over');
  }

  function handleDragLeave(e) {
    this.classList.remove('over');
  }

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });
});

There are a couple of points worth covering in this code:

  • The default action for dragover event is to set the dataTransfer.dropEffect property to "none". The dropEffect property is covered later on this page. For now, just know that it prevents the drop event from firing. To override this behavior, call e.preventDefault(). Another good practice is to return false in that same handler.

  • The dragenter event handler is used to toggle the over class instead of dragover. If you use dragover, the event fires repeatedly while the user holds the dragged item over a column, causing the CSS class to toggle repeatedly. This makes the browser do a lot of unnecessary rendering work, which can affect the user experience. We strongly recommend minimizing redraws, and if you need to use dragover, consider throttling or debouncing your event listener.

Complete the drop

To process the drop, add an event listener for the drop event. In the drop handler, you'll need to prevent the browser's default behavior for drops, which is typically some sort of annoying redirect. To do this, call e.stopPropagation().

function handleDrop(e) {
  e.stopPropagation(); // stops the browser from redirecting.
  return false;
}

Be sure to register the new handler alongside the other handlers:

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });

If you run the code at this point, the item doesn't drop to the new location. To make that happen, use the DataTransfer object.

The dataTransfer property holds the data sent in a drag action. dataTransfer is set in the dragstart event and read or handled in the drop event. Calling e.dataTransfer.setData(mimeType, dataPayload) lets you set the object's MIME type and data payload.

In this example, we're going to let users rearrange the order of the columns. To do that, first you need to store the source element's HTML when the drag starts:

function handleDragStart(e) {
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

In the drop event, you process the column drop by setting the source column's HTML to the HTML of the target column that you dropped the data on. This includes checking that the user isn't dropping back onto the same column they dragged from.

function handleDrop(e) {
  e.stopPropagation();

  if (dragSrcEl !== this) {
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

You can see the result in the following demo. For this to work, you'll need a desktop browser. The Drag and Drop API isn't supported on mobile. Drag and release the A column on top of the B column and notice how they change places:

More dragging properties

The dataTransfer object exposes properties to provide visual feedback to the user during the drag process and control how each drop target responds to a particular data type.

  • dataTransfer.effectAllowed restricts what 'type of drag' the user can perform on the element. It's used in the drag-and-drop processing model to initialize the dropEffect during the dragenter and dragover events. The property can have the following values: none, copy, copyLink, copyMove, link, linkMove, move, all, and uninitialized.
  • dataTransfer.dropEffect controls the feedback that the user gets during the dragenter and dragover events. When the user holds their pointer over a target element, the browser's cursor indicates what type of operation is going to take place, such as a copy or a move. The effect can take one of the following values: none, copy, link, move.
  • e.dataTransfer.setDragImage(imgElement, x, y) means that instead of using the browser's default 'ghost image' feedback, you can set a drag icon.

File upload

This simple example uses a column as both the drag source and drag target. This might happen in a UI that asks the user to rearrange items. In some situations, the drag target and source might be different element types, as in an interface where the user needs to select one image as the main image for a product by dragging the selected image onto a target.

Drag and Drop is frequently used to let users drag items from their desktop into an application. The main difference is in your drop handler. Instead of using dataTransfer.getData() to access the files, their data is contained in the dataTransfer.files property:

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; (f = files[i]); i++) {
    // Read the File objects in this FileList.
  }
}

You can find more information about this in Custom drag-and-drop.

More resources