Drag operations

Central to the Drag and Drop API are the various drag events that fire in a specific order and are expected to be handled in a specific way. This document describes the steps that occur during a drag and drop operation, and what the application is supposed to do within each handler.

At a high level, here are the possible steps in a drag and drop operation:

  • The user initiates the drag on a source node; the dragstart event is fired on the source node. Within this event, the source node prepares the context for the drag operation, including the drag data, feedback image, and allowed drop effects.
  • The user drags the item around: every time a new element is entered, the dragenter event is fired on that element, and the dragleave event is fired on the previous element. Every few hundred milliseconds, a dragover event is fired on the element the drag is currently inside, and the drag event is fired on the source node.
  • The drag enters a valid drop target: the drop target cancels its dragover event to indicate that it is a valid drop target. Some form of drop feedback indicates the expected drop effect to the user.
  • The user performs the drop: the drop event is fired on the drop target. Within this event, the target node reads the drag data.
  • The drag operation ends: the dragend event is fired on the source node. This event is fired regardless of whether the drop was successful or not.

Starting a drag

The drag starts on a draggable item, which can be a selection, a draggable element (including links, images, and any element with draggable="true"), a file from the operating system's file explorer, etc. First, the dragstart event is fired on the source node, which is the draggable element or, for selections, the text node that the drag started on. If this event is cancelled, then the drag operation is aborted. Otherwise, the pointercancel event is also fired on the source node.

The dragstart event is the only time you can modify the dataTransfer. For a custom draggable element, you almost always want to modify the drag data, which is covered in detail in Modifying the drag data store. There are two other things you can change: the feedback image and the allowed drop effects.

In this example, we add a listener for the dragstart event by using the addEventListener() method.

html
<p draggable="true">This text <strong>may</strong> be dragged.</p>
js
const draggableElement = document.querySelector('p[draggable="true"]');
draggableElement.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", "This text may be dragged");
});

You could also listen to a higher ancestor as drag events bubble up as most other events do. For this reason, it is common to also check the event's target, so that dragging a selection contained within this element does not trigger the setData (although selecting text within the element is hard, it is not impossible):

js
draggableElement.addEventListener("dragstart", (event) => {
  if (event.target === draggableElement) {
    event.dataTransfer.setData("text/plain", "This text may be dragged");
  }
});

Setting the drag feedback image

When a drag occurs, a translucent image is generated from the source node, and follows the user's pointer during the drag. This image is created automatically, so you do not need to create it yourself. However, you can use setDragImage() to specify a custom drag feedback image.

js
draggableElement.addEventListener("dragstart", (event) => {
  event.dataTransfer.setDragImage(image, xOffset, yOffset);
});

Three arguments are necessary. The first is a reference to an image. This reference will typically be to an <img> element, but it can also be to a <canvas> or any other element. The feedback image will be generated from whatever the image looks like on screen, although for images, they will be drawn at their original size. The second and third arguments to the setDragImage() method are offsets where the image should appear relative to the mouse pointer.

It is also possible to use images and canvases that are not in a document. This technique is useful when drawing custom drag images using the canvas element, as in the following example:

js
draggableElement.addEventListener("dragstart", (event) => {
  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = 50;

  const ctx = canvas.getContext("2d");
  ctx.lineWidth = 4;
  ctx.moveTo(0, 0);
  ctx.lineTo(50, 50);
  ctx.moveTo(0, 50);
  ctx.lineTo(50, 0);
  ctx.stroke();

  event.dataTransfer.setDragImage(canvas, 25, 25);
});

In this example, we make one canvas the drag image. As the canvas is 50×50 pixels, we use offsets of half of this (25) so that the image appears centered on the mouse pointer.

Dragging over elements and specifying drop targets

For the entire course of the drag operation, all device input events (such as mouse or keyboard) are suppressed. The dragged data can be moved over various elements in the document, or even elements in other documents. Whenever a new element is entered, a dragenter event is fired on that element, and a dragleave event is fired on the previous element.

Note: dragleave always fires after dragenter, so conceptually, in between these two events, the target has entered a new element but has not exited the previous one yet.

Every few hundred milliseconds, two events fire: a drag event at the source node, and a dragover event at the element the drag is currently inside. Most areas of a web page or application are not valid places to drop data, so elements by default ignore any drop that happened on it. The element can elect itself to be a valid drop target by cancelling the dragover event. If the element is an editable text field, such as a <textarea> or <input type="text">, and the data store contains one text/plain item, then the element is a valid drop target by default without cancelling dragover.

html
<div id="drop-target">You can drag and then drop a draggable item here</div>
js
const dropElement = document.getElementById("drop-target");

dropElement.addEventListener("dragover", (event) => {
  event.preventDefault();
});

Note: The spec requires the dragenter event to be cancelled too for a drop target, otherwise the dragover or dragleave events won't even start firing on this element; in practice no browser implements this, and the "current element" changes every time a new element is entered.

Note: The spec requires that cancelling the drag event aborts the drag; in practice no browser implements this. See example below:

However, you usually only want the drop target to accept drops in certain situations (for example, only if a link is being dragged). To do this, check a condition and only cancel the event when the condition is met. For example, you can check if the dragged data contains links:

js
dropElement.addEventListener("dragover", (event) => {
  const isLink = event.dataTransfer.types.includes("text/uri-list");
  if (isLink) {
    event.preventDefault();
  }
});

In this example, we use the includes method to check if the type text/uri-list is present in the list of types. If it is, we will cancel the event so that a drop may be allowed. If the drag data does not contain a link, the event will not be cancelled, and a drop cannot occur at that location.

Drop feedback

Now the user is dragging into a valid drop target. There are several ways in which you can indicate to the user that a drop is allowed at this location, and what might happen if the drop happens. Usually, the mouse pointer will update as necessary depending on the value of the dropEffect property. Although the exact appearance depends on the user's platform, typically a plus sign icon will appear for a copy for example, and a "cannot drop here" icon will appear when a drop is not allowed. This mouse pointer feedback is sufficient in many cases.

Drop effects

When dropping, there are several operations that may be performed:

copy

The data will be simultaneously present at the source and target locations after dropping.

move

The data will only be present at the target location, and will be removed from the source location.

Some form of linking will be created between the source and drop locations; there is only one instance of the data at the source location.

none

Nothing happens; the drop failed.

With the dragenter and dragover events, the dropEffect property is initialized to the effect that the user is requesting. The user can modify the desired effect by pressing modifier keys. Although the exact keys used vary by platform, typically the Shift and Control keys would be used to switch between copying, moving, and linking. The mouse pointer will change to indicate which operation is desired. For instance, for a copy, the cursor might appear with a plus sign next to it.

You can modify the dropEffect property during the dragenter or dragover events, if for example, a particular drop target only supports certain operations. You can modify the dropEffect property to override the user effect, and enforce a specific drop operation to occur.

js
target.addEventListener("dragover", (event) => {
  event.dataTransfer.dropEffect = "move";
});

In this example, move is the effect that is performed.

You can use the value none to indicate that no drop is allowed at this location. You should usually do this if the element is only temporarily not accepting drops; if it's not intended to be a drop target, you should just not cancel the event.

Note that setting dropEffect only indicates the desired effect at this particular instant; a later dragover dispatch may change it. To persist the choice, you must set it in every dragover event. Also, this effect is only informational, and what effects ends up being implemented depends on both the source and the target nodes (for example, if the source node cannot be modified, then even if a "move" effect is requested, it may not be possible).

For both user gestures and programmatically setting dropEffect, by default, all three drop effects are available. The draggable element can restrict itself to only allow certain effects by setting the effectAllowed property within a dragstart event listener.

js
draggableElement.addEventListener("dragstart", (event) => {
  event.dataTransfer.effectAllowed = "copyLink";
});

In this example, only a copy or link operation is allowed, but a move operation is not possible to be selected either via script or via user gestures.

The values of effectAllowed are combinations of dropEffect:

none

No operation is permitted

copy

copy only

move

move only

link only

copyMove

copy or move only

copy or link only

linkMove

link or move only

all

copy, move, or link

uninitialized

The default value when the effect has not been set; generally equivalent to all, except the default dropEffect may not always be copy.

By default, the dropEffect is initialized based on effectAllowed, in the order of copy, link, move, selecting the first one that is allowed. The unselected but allowed effects may also be selected as default if appropriate; for example, on Windows, holding the Alt key causes link to be used in priority. If effectAllowed is uninitialized and the dragged element is an <a> link, the default dropEffect is link; if effectAllowed is uninitialized and the dragged element is a selection from an editable text field, the default dropEffect is move.

Custom drop feedback

For more complex visual effects, you can perform other operations during the dragenter event. For example, by inserting an element at the location where the drop will occur. This might be an insertion marker, or an element that represents the dragged element in its new location. To do this, you could create an <img> element and insert it into the document during the dragenter event.

The dragover event will fire at the element the mouse is pointing at. Naturally, you may need to move the insertion marker around a dragover event as well. You can use the event's clientX and clientY properties as with other mouse events to determine the location of the mouse pointer.

Finally, the dragleave event will fire at an element when the drag leaves the element. This is the time when you should remove any insertion markers or highlighting. You do not need to cancel this event. The dragleave event will always fire, even if the drag is cancelled, so you can always ensure that any insertion point cleanup can be done during this event.

For a practical example of using these events, see our Kanban board example.

Performing a drop

When the user releases the mouse, the drag and drop operation ends.

In order for the drop to be potentially successful, the drop must happen over a valid drop target, and the dropEffect must not be none at the time of mouse release. Otherwise, the drop operation is considered failed.

If the drop is potentially successful, a drop event is fired on the drop target. You need to cancel this event using preventDefault() in order for the drop to be considered actually successful. Otherwise, the drop is also considered successful if the drop was dropping text (the data contains a text/plain item) into an editable text field. In this case, the text is inserted into the field (either at the cursor position or at the end, depending on platform conventions) and, if the dropEffect is move while the source is a selection within an editable region, the source is removed. Otherwise, for all other drag data and drop targets, the drop is considered failed.

During the drop event, you should retrieve that data that was dropped from the event and insert it at the drop location. You can use the dropEffect property to determine which drag operation was desired. The drop event is the only time when you can read the drag data store, other than dragstart.

js
target.addEventListener("drop", (event) => {
  event.preventDefault();
  const data = event.dataTransfer.getData("text/plain");
  target.textContent = data;
});

In the example here, once the data has been retrieved, we insert the string as the textual content of the target. This has the effect of inserting the dragged text where it was dropped, assuming that the drop target is an area of text such as a p or div element.

An empty string will be returned if data of that type does not exist. (Naturally, though, you would likely know that the right type of data was available, as it was previously checked during a dragover event.)

You can retrieve other types of data as well. If the data is a link, it should have the type text/uri-list. You could then insert a link into the content.

js
target.addEventListener("drop", (event) => {
  event.preventDefault();
  const lines = event.dataTransfer.getData("text/uri-list").split("\r\n");
  lines
    .filter((line) => !line.startsWith("#"))
    .forEach((line) => {
      const link = document.createElement("a");
      link.href = line;
      link.textContent = line;
      target.appendChild(link);
    });
});

for more information about how to read, see Working with the drag data store.

It is also the source and the target elements' responsibility to collaborate to implement the dropEffect—the source listens for the dragend event and the target listens for the drop event. For example, if the dropEffect is move, then one of these elements must remove the dragged item from its old location (usually the source element itself, because the target element doesn't necessarily know or have control over the source).

A failed drop

The drag-and-drop operation is considered failed if one of the following is true:

  1. The user pressed the Escape key
  2. The drop happened outside of a valid drop target
  3. The drop effect was none at the time of mouse release
  4. The drop event was not cancelled and the drop was not dropping text (containing a text/plain data) into an editable text field (see performing a drop)

For cases 1 and 3, if the abortion happens while hovering over a valid drop target, the drop target receives a dragleave event, as if the drop no longer happens over it, so that it could clean up any drop feedback. In all cases, the dropEffect is set to none for subsequent events.

Afterwards, a dragend event is fired at the source node. An animation of the dragged selection going back to the source of the drag-and-drop operation may be displayed.

Finishing the drag

Once the drag is complete, a dragend event is fired at the source of the drag (the same element that received the dragstart event). This event will fire if the drag was successful or if it was cancelled. However, you can use the dropEffect property to determine which drop operation occurred.

If the dropEffect property has the value none during a dragend, then the drag was cancelled. Otherwise, the effect specifies which operation was performed. The source can use this information after a move operation to remove the dragged item from the old location.

A drop can occur inside the same window or over another application. The dragend event will always fire regardless. The event's screenX and screenY properties will be set to the screen coordinates where the drop occurred.

After the dragend event has finished propagating, the drag and drop operation is complete.

See also