Let's Learn HTML Drag and Drop API

In this article we'll learn all about the Drag and Drop API in modern browsers

By: Ajdin Imsirovic 16 June 2021

In this post we’ll explore the Drag and Drop API in modern browsers until we get comfortable using this functionality.

Drag and drop computer interaction abstract concept image

Understanding HTML Drag and Drop API

Today, in many web apps, the following functionality is almost the expected behavior: a user is “spoiled” to intuitively know that if they click and drag an element, then drop it onto another element, it will probably work (if it makes sense to perform such an action).

A simple, useful example, is building a quiz interface, where we would need to order separate words onto a section of the screen, and we would need to put those words in order - so as to get a passing score.

In this section, we’ll list some basic examples of the Drag and Drop API, as well as some basic terminology.

This API is all about two groups of user-triggered events:

  1. the event of dragging a source element
  2. the event of dropping onto a target element

The first group of user-tiggered events, i.e “the source events”, are dragstart, drag, and dragend.

The second group of user-triggered events, i.e “the target events”, are dragenter, dragover, dragleave, and drop.

Let’s go through a possible drag and drop sequence of events:

  1. The user clicks the element, holds the click, and starts moving the element on the page
  2. The dragged (source) element enters the coordinates of another element.
  3. The source element is dragged over that “another” element.
  4. The source element leaves the area of the “another” element.
  5. The source element enters the coordinates of a third (target) element.
  6. The source element is over the target element and the user releases the left mouse button.

We can “translate” the above scenario into the sequence of events triggered:

  1. dragstart (emitted from the source element), immediately followed by drag (from the source element). The drag event is triggered all the time, as long as the user is moving the element - similar to mousemove
  2. dragenter (emitted from the target element)
  3. dragover (from target)
  4. dragleave (from target)
  5. dragenter (from another target)
  6. drop (from another target) and dragend (from source element)

Drag and drop event handlers

Although the above-listed events are fired, nothing “perceptable as a real interaction” happens for the user dragging the source element onto the target element - unless we use the Drag and drop API’s event handlers.

Here are the available event handlers:

  1. ondrag
  2. ondragend
  3. ondragenter
  4. ondragexit
  5. ondragleave
  6. ondragover
  7. ondragstart
  8. ondrop

Due to such a straightforward naming convention, it’s easy to connect the event handlers with their respective events on this API.

Making an element draggable (converting a plain HTML element to a source element)

To make an element into a source element, we need to set its draggable property to true, like this:

1
2
3
<span id="drag-it" draggable="true">
    Drag this source element around!
</span>

Let’s also style it a bit:

1
2
3
4
5
#drag-it {
  background: yellowgreen;
  padding: 10px 20px;
  display: inline-block;
}

Adding the target element

Let’s add another element: this is our target element.

1
2
3
<div id="drop-it">
    Drop the source element to this target element!
</div>

And let’s add its corresponding CSS:

1
2
3
4
5
6
7
8
9
10
#drop-it {
  background: lightgray;
  padding: 10px 20px;
  display: inline-block;
  position: absolute;
  bottom: 0;
  left: 0;
  height: 50%;
  width: 50%;
}

If we try click-dragging the source element at this point, it will work - since we’ve made it draggable. But other than that, right now it doesn’t do much else. It’s time to hook into those events.

Hooking into drag and drop events with JS

Let’s define a function that will run on the source element.

function dragStarted(event) {
    console.log(event);
}

It’s as simple as it can get.

Now let’s listen for it in an HTML attribute:

<span
    id="drag-it"
     draggable="true"
     ondragstart="dragStarted(event)"
>
    Drag this source element around!
</span>

We’ve updated the HTML attributes on our source element; specifically, we’ve added an ondragstart event handler attribute. Then we passed it a value: the name of the function to run, with the built-in event parameter passed to the function call.

Testing this with the browser console open, we should see something like the following output:

// [object DragEvent]
{
    "isTrusted": true
}

Great, we have just verified our code will run when a dragstart event fires. Let’s now do something more “constructive”:

function dragStarted(evt) {
    console.log(evt);
    event.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}

Now, if a user clicks and drags the source element, it will change the element’s color to rgba(0,200,100,0.25) as the result of handling the dragstart event.

This “takes care” of the dragstart event. Let’s now deal with the dragover event.

First, we’ll add the event handler as an HTML attribute:

<div
     id="drop-it"
     draggable="true"
     ondragover="allowDrop(event)"
>
    Drag the source div onto this target element!
</div>

Next, we’ll define the event handler function that was passed in to the ondragover attribute:

function allowDrop(evt) {
  console.log(event);
}

If we tested the current code by dragging the source element over to the target element, we’ll see the expected output:

// [object DragEvent]
{
    "isTrusted": true
}

Now that we’ve set this up, we need to, again, do something “more constructive” in our event handler. This time, we need to prevent the default behavior, because for some elements, browsers do not allow dropping source onto target. This built-in browser functionality will disallow what we’re attempting to do in this exercise. To “switch off” this default browser functionality, we’ll use the preventDefault() function call:

function allowDrop(evt) {
  console.log(event);
  event.preventDefault();
}

This has now prepared us for handling the next event, the drop event.

Again, we’ll begin by adding the ondrop attribute, and assigning it a value of the event handler function call:

<div
     id="drop-it"
     draggable="true"
     ondragover="draggedOver(event)"
     ondrop="dropped(event)"
>
    Drag the source div onto this target div!
</div>

Now let’s define our dropped() function:

function dropped(evt) {
    console.log("dropped");
}

This time, we’re simply logging out the word “dropped” so as to differentiate this event handler from the other console.log(event) pieces of code, as they all just return // [object DragEvent] etc.

Now, we’ll again update the definition of the dropped() function to something more useful.

Let’s begin by stopping event propagation, so that the event does not bubble up the DOM tree. We’re also preventing default browser behavior, to make sure we’ve reset any weird operations.

function dropped(evt) {
  console.log("dropped");
  event.stopPropagation();
  event.preventDefault();
}

Next, we’ll need to actually drop the source to target. We’ll do this with the help of the dataTransfer object.

Drag and drop data with the dataTransfer object and drop effects

In the context of the Drag and Drop API, the data transfer object is a property on the specific Event object.

If we say that we’re working with an instance of the Event object, called evt, then the data transfer object can be accessed like this:

evt.dataTransfer

Let’s look at a drag-and-drop operation from a different angle.

When we drag an element and look for a place to drop it on the screen, what is it that we are actually doing?

We’re working with data, specifically, with the setData() method on the dataTransfer object.

In other words, in a drag-and-drop operation, a user triggers drag and drop events.

We write the code to handle such events using event handlers that we define ourselves.

We pass the specific Event object’s instance to those event handlers, and then access the dataTransfer object (a property on a given Event object’s instance), and then access the dataTransfer object’s setData() method.

The setData() method accepts two parameters:

  1. the MIME type of the data passed in
  2. the data itself

The first parameter is there to give some constraints on how the passed-in data should look. It’s almost like a validation parameter. If we specify the MIME type as text in the first parameter of setData(), then that MIME type is what is expected to be passed in as the second parameter.

The data stored in the dataTransfer object also allows us to take care of the so-called drop effects - visual cues for the user on what is about to happen if they complete the drag-and-drop operation.

The drop effect has four distinct varieties:

  1. copy
  2. link
  3. move
  4. none

The first one copies the source element into the target element.

The second one adds a link to the source element, from the target element.

The third one moves the source element into the target element.

The fourth one doesn’t allow the drop operation to complete.

So, in a nutshell, what we want to do in the next step is to “scoop up” the source element’s data when the drag and drop action begins (i.e in the dragstart event). After we’ve set this data using setData(), we next read it when the drop event fires.

Thus, let’s first extend the definition of our dragStarted() event handler, like this:

function dragStarted(evt) {
  evt.dataTransfer.setData("text", evt.target.id);
  evt.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}

Basically, we’re acessing the given Event object’s instance, called evt in the above function declaration.

Next, we’re accessing the dataTransfer object, and then accessing its setData method. All in all, we’re doing this:

evt.dataTransfer.setData()

Next, as already described above, we’re setting the MIME type to text, and we’re setting the data source to be evt.target.id.

Let’s try logging this out, to see what comes up:

function dragStarted(evt) {
  console.log("We're setting this data:", evt.target.id);
  evt.dataTransfer.setData("text", evt.target.id);
  evt.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}

Here’s the output in the console based on the above improvement:

"We're setting this data:" "drag-it"

We’ll now need to “receive” this data in the drop event handler, i.e in the dropped() function definition, which we’ll now update to this:

function dropped(evt) {
  evt.preventDefault();
  const data = evt.dataTransfer.getData("text");
  evt.target.appendChild(document.getElementById(data));
}

We’re overriding the built-in browser behavior with preventDefault() and then we’re getting the data from the dataTransfer object. Finally, we’re appending this data onto the target element.

Let’s add another console.log to inspect what the gotten data looks like:

function dropped(evt) {
  evt.preventDefault();
  const data = evt.dataTransfer.getData("text");
  console.log(data);
  evt.target.appendChild(document.getElementById(data));
}

The output of the console.log improvement now is this:

"drag-it"

Finally, let’s wrap it up with an overview of the complete code, with a second draggable element added:

Here’s the HTML:

<span
    id="drag-it"
    draggable="true"
    ondragstart="dragStarted(event)"
>
    Drag this source element around!
</span>
<span
    id="drag-it-2"
    draggable="true"
    ondragstart="dragStarted(event)"
>
    Fruit
</span>

<div
    id="drop-it"
    ondrop="dropped(event)"
    ondragover="allowDrop(event)"
>
  Drop the source element onto this target element!
</div>

Here’s the CSS:

#drop-it {
  width: 768px;
  height: 350px;
  padding: 10px;
  border: 1px solid #aaaaaa;
  position: absolute;
  top: 100px;
}
#drag-it, #drag-it-2 {
  background: yellowgreen;
  padding: 10px 20px;
  display: inline-block;
  font-size: 35px;
  line-height: 50px;
  display: inline-block;
  margin: 20px;
}

And here’s the JS:

function allowDrop(evt) {
  evt.preventDefault();
  evt.currentTarget.style.backgroundColor = "rgba(200,200,200, 1)";
}

function dragStarted(evt) {
  console.log("We're setting this data:", evt.target.id);
  evt.dataTransfer.setData("text", evt.target.id);
  evt.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}

function dropped(evt) {
  evt.preventDefault();
  const data = evt.dataTransfer.getData("text");
  console.log(data);
  evt.target.appendChild(document.getElementById(data));
}

This code is also available online under a codepen titled Simplest drag and drop.



Note:
This exercise comes from Book 5 of my book series on JS, available on Leanpub.com.



Feel free to check out my work here: