DEV Community

Cover image for 🚀 A powerful drag and drop implementation in just 16 lines of JavaScript
Siddharth
Siddharth

Posted on • Originally published at blog.siddu.tech on

🚀 A powerful drag and drop implementation in just 16 lines of JavaScript

Drag and drop is a very useful tool in applications as it can simplify a large part of the process for users. It's also a common task we delegate to other libraries which may bloat your app when you just need a very simple implementation when you can use the Drag and Drop Web API. Today, I'll show you how you can do just that!

What we are making

This is the basic implementation we are aiming to build:

That was made in just 16 lines of JavaScript!

And with a few more lines we can add a lot more bonus features! Here's a demo with some more features!

Play around with it, you will see that we can

  • Drop elements only in certain places
  • Style the element in which we are dropping
  • Style the original copy of the dragged element
  • (With a little trickery) even style the dragged element!

All this with just 30 lines of code!

It works on almost all desktop browsers with partial support all the way back to IE 6(!) which should be enough for this to work, but it doesn't work on some mobile browsers.

You can see the up-to-date CanIUse data here:

Data on support for the drag and drop feature across the major browsers from caniuse.com

Dealing with libraries for this very basic functionality has been a pain for me, and to save you the trouble I thought I'd document the process here!

HTML structure

Places to drag into

You need some drop targets to be able to drag something into them right? We can add these using regular divs:

<div class='drop'></div>
<div class='drop'></div>
<div class='drop'></div>
<div class='drop'></div>

Enter fullscreen mode Exit fullscreen mode

Note: I'll be referring to the places we can drop into as drop targets in this post

You can add as many of them wherever you like, as long as an element has the drop class we will be able to drop into them.

We can also add some basic styles for them to look nice.

* {
    box-sizing: border-box;
    font-family: sans-serif;
}

.drop {
    width: 220px;
    height: 45px;
    background: #2563EB;
    margin-bottom: 10px;
    padding: 10px;
    border-radius: 3px;
}

Enter fullscreen mode Exit fullscreen mode

The element to drag

For an element to be draggable, we need, well, an element! We can place the element in one of the drop targets we made before. This is how it should look like

<div class='drop'>
    <div id='drag' draggable='true' ondragstart='event.dataTransfer.setData('text/plain', null)'>
        Drag me!
    </div>
</div>
<div class='drop'></div>
<div class='drop'></div>
<div class='drop'></div>

Enter fullscreen mode Exit fullscreen mode

Notice how we also set the draggable attribute to true. Every draggable element needs to have the draggable attribute set for it to be draggable.

Also, not every element can be dragged even if the draggable attribute is set. We need to explicitly say that the element is draggable by listening to the dragstart event in the HTML. There we are setting null as we don't have any data to share and we are setting the data type text/plain.

We can (again) also add some basic styles for them to look nice.

#drag {
    width: 200px;
    height: 25px;
    border-radius: 3px;
    background: black;
    color: white;
    display: grid;
    align-items: center;
    justify-content: center;
}

Enter fullscreen mode Exit fullscreen mode

Note that as long as an element has the draggable attribute set to true and the drop targets have the drop class, the below code should work everywhere

The minimal implementation

For our drag and drop to be functional, we just need 3 different event listeners. Everything else is a bonus.

First, we need to store the element we are dragging. We can do this by listening to the dragstart event.

let dragged;

document.addEventListener('dragstart', event => {
    dragged = event.target;
}, false)

Enter fullscreen mode Exit fullscreen mode

Whenever an element is dragged, this will store the dragged element in a variable.

Next, we can listen to drop events so we can drop elements.

document.addEventListener('drop', event => {
    // Prevent default behaviour (sometimes opening a link)
    event.preventDefault();

    if (event.target.className === 'drop') {
        dragged.parentNode.removeChild(dragged);
        event.target.appendChild(dragged);
    }
}, false)

Enter fullscreen mode Exit fullscreen mode

Whenever we drop an element, if the element is a drop target (has the drop class) we will append the dragged element to the drop target.

We're almost done, but we need to do one more thing to make this work.

By default, dragging elements does nothing, so to prevent the default behavior we need to call event.preventDefault whenever we drag over the drop target.

This is easy to achieve with a oneliner:

document.addEventListener('dragover', event => event.preventDefault(), false);

Enter fullscreen mode Exit fullscreen mode

That's it! In 16 lines we have functional drag and drop!

Here's a video of it in action:

Minimal drag and drop

Adding more features

Even if this drag and drop works, it's not very nice. It doesn't seem very "natural". Luckily, in a few lines of code, we can make this drag and drop even better!

Styling the original dragged element

Whenever we drag an element, the original copy of the element doesn't change its style. It would look better if we could add a different style to these dragged elements, like making them transparent to show that it is being dragged.

This is very easy to do. Just add the styles in the dragstart event listener.

document.addEventListener('dragstart', event => {
    // ...

    event.target.style.opacity = 0.5;
    // add more styles as you like...

    // ...
});

Enter fullscreen mode Exit fullscreen mode

But we also need to reset the style once we finish dragging. We can do that by listening to dragend:

document.addeventListener('dragend', event => event.target.style.opacity = '', false)

Enter fullscreen mode Exit fullscreen mode

Styling the drop targets

We can also style the drop target by listening to the dragenter event:

document.addEventListener('dragenter', event => {
    if (event.target.className === 'drop') event.target.style.background = '#2c41cc';
}, false)

Enter fullscreen mode Exit fullscreen mode

Once again, we need to reset the styles once we leave the element. We can do that by listening to dragleave:

document.addEventListener('dragleave', event => {
    if (event.target.className === 'drop') event.target.style.background = '';
}, false)

Enter fullscreen mode Exit fullscreen mode

We also need to reset the styles once we drop the event. We can edit the drop event to achieve this.

document.addEventListener('drop', event => {
    // ...

    if (event.target.className === 'drop') {
        event.target.style.background = '';
    //...
})

Enter fullscreen mode Exit fullscreen mode

Styling the dragged copy

With a bit of trickery, we can style the dragged copy too! Maybe we can rotate the element a bit to make it a bit more natural.

We can do this by styling the original copy and immediately undoing those styles in the dragstart event so that the users don't see it.

listen('dragstart', event => {
    // ...

    event.target.style.transform = 'rotate(-2deg)';
    setTimeout(() => event.target.style.transform = '', 1);
})

Enter fullscreen mode Exit fullscreen mode

Now the dragged copy will appear to be rotated when we are dragging it!

You now have a fully functioning drag and drop implementation!

Here's a video of it in action:

Complete drag and drop.gif

Here's a Gist with all the source code for reference

Conclusion

We took a task for which we very commonly delegate to libraries and implemented it ourselves, with surprisingly little code.

I hope this opens your eyes to how much you can do with just vanilla JS. You don't need libraries every time.

Have you ever tried implementing drag and drop on your own? Share in the comments!

Discussion (11)

Collapse
jonrandy profile image
Jon Randy • Edited on

Very buggy. I got into this situation fairly quickly...
Screen grab
And this...
Screen grab

Collapse
siddharthshyniben profile image
Siddharth Author

I don't seem to able to reproduce it. Which browser are you in?

Collapse
jonrandy profile image
Jon Randy • Edited on

Happens on all browsers I've tried - Firefox and Chrome on both macOS and Ubuntu. Easy to reproduce. I think it has to do with not disabling user selection on the draggable items (just a hunch, I don't have time to look into it)

Thread Thread
siddharthshyniben profile image
Siddharth Author

I see, unfortunately I've tried a long time but I never was able to reproduce.

Anyways, thanks for the feedback! I'll look into it and see if I can do something about it.

Collapse
hexdom profile image
Helge-Kristoffer Wang • Edited on

I was able to get something similar. I separated the text into its own box and the black background into another box.

dev-to-uploads.s3.amazonaws.com/up...

Either way, this is a good start for a drag and drop component - it does work, but might need a little fine tuning.

Google Chrome Version 95.0.4638.69 (Official Build) (x86_64)

Thread Thread
siddharthshyniben profile image
Siddharth Author

Can't reproduce, but thanks for the feedback. I'll look into this, and once i find out the issues I'll write a follow up!

Collapse
mindplay profile image
Rasmus Schultz

It's kind of the wrong API for custom drag-and-drop operations - it's very limited in terms of what you can do with it, visually, and provides no real means of fine tuning the experience in terms of interaction either. It will let you go about as far as you went in this demo, but not much further.

I wouldn't recommend using the drag events API unless you need to drag to/from the desktop - for file uploads and such, about the only thing this API does well.

Unhappy with the complexity of drag-and-drop libraries myself, I started this library about four years ago:

github.com/mindplay-dk/zero-drag

It ended up a lot more complex than I would have liked - even if it is less than 200 lines of actual code (about 1K minified) when you get into all the details of these interactions (and even if this is a lot simpler than the libraries that also deal with the visuals) it's just a lot more complex than it seems. (On the up side, if you look at the examples of using it, those are about as simple as using the standard API.)

Note that this doesn't support touch events - I think that was the last remaining big push to finish this, but I've had no reason or inspiration to actually finish.

Maybe it inspires somebody else to do it. I'm a sure a lot has to do with taste, but I've really never seen a drag-and-drop library or approach that I really liked. 😉

Collapse
siddharthshyniben profile image
Siddharth Author

You are totally right, when we need customization this doesn't really work (like all native APIs 🤣). When you need a fully featured approach libraries may be the right way to go.

This does work for when you just need the minimal functionality. I was just pointing this out for awareness because I don't want you to bloat your code :D

Hoping in the future this API will get better tho, if it does it will be awesome!

Collapse
deathshadow60 profile image
deathshadow60 • Edited on

I'd suggest adding {} around the outside creating a scope so LET serves a purpose. "AppendChild" will automatically do a "removeChild" so you don't need that line. The overhead of the "function for nothing" is doing you no favors, the use of "target" instead of "currentTarget" is why some people are seeing behavioral bugs. Not even sure why you're messing around with setting the data type to text/plain... did you just blindly copy that part from a tutorial?

Though the worst problem is the hardcoding in the markup of the event preventing re-usability, lack of target restrictions, and that you hooked "document" instead of the actual elements!

Here's a "fixed" fork.

codepen.io/jason-knight/pen/poWzbNJ

Uses data- attributes so that each "set" can self-target. A lot of the logic you were tracking isn't necessary because it targets elements, it auto-hooks itself to all relevant elements, doesn't have those pesky false drop targets because again, it hooks the elements with Event.currentTarget not document and Event.target, etc, etc.

Don't use document events for individual elements no matter what some folks out there say. Likewise don't use Event.target it can give you the wrong origin and/or destination if a cascade of elements is involved. You start putting tags inside your draggables, those elements will report as Event.target.

Remember, Event.target is the lowest element clicked on. Event.currentTarget is the Element the event is attached to!

Oh and avoid garbage like Element.style if you can. Unless you're actually calculating values, that too is just bad code. Why? 1) Presentation has no more business in your JS than it does your HTML, 2) it makes it harder for people to style it since you have to dive into logic to just set the colours.

Anyhow, the script's not even 100 bytes larger despite all the added functionality.

Collapse
siddharthshyniben profile image
Siddharth Author

Wow that's a lot of fixes! Thanks for sharing your insightful thoughts 😃

Collapse
prabhukadode profile image
Prabhu

Will try this out