Drag and Drop in HTML5 and JavaScript: A Complete Guide with Real-World Examples

- Andrés Cruz - ES En español

Drag and Drop in HTML5 and JavaScript: A Complete Guide with Real-World Examples

The Drag and Drop system is one of those features that, when well implemented, elevates the user experience without requiring external libraries. I've been using it for years in real projects — from reordering to-do lists to uploading files in interfaces connected to Laravel or Django backends — and I'm still amazed at how flexible it can be with just HTML5 and vanilla JavaScript.

What is Drag and Drop in HTML5? It is a user interface that allows moving elements from one place to another using the mouse or a finger. On the web, HTML5 makes it native through the DataTransfer API, without the need for third-party libraries.

In this guide, I'll explain everything to you: from fundamental events to advanced, production-ready examples, so you can implement DnD in minutes and master it in hours.

Earlier, we saw how to generate files in JavaScript, as well as read and manipulate them.

What Drag and Drop is and why it remains so relevant

Drag and Drop allows grabbing an element, moving it, and dropping it somewhere else in the interface. Although it sounds simple, it is one of the most intuitive interactions for users because it exactly replicates what we do in the physical world: pick up an object and put it somewhere else.

How the drag and drop interaction works

The system relies on three key pieces that must always be present:

  • Draggable element (draggable): the DOM element that the user can grab and move.
  • Data to transfer (dataTransfer): the payload of information that "travels" along with the element while being dragged.
  • Drop target (drop zone): the container that receives the element when dropped.

Actions are immediate and natural: you drag an element, drop it, and the interface reacts instantly.

Modern use cases for Drag and Drop

  • Reordering Kanban boards or to-do lists.
    • I regularly use this in my courses and books, implementing a backend with Laravel or Django to persist the new order in the database.
  • Building Trello-style or Gmail-style interfaces.
  • Uploading files by dragging them directly into a container (without <input type="file">).
  • Moving images between galleries.
  • Backoffice interfaces with repositionable blocks, such as page editors or form builders.

Drag and Drop events in JavaScript: clear, straightforward explanation

The drag and drop API uses seven key events — and that's it. With practice, you'll memorize them naturally. Let's look at them organized by their role:

  • Events on the dragged element
    • dragstart: fires once when the user begins dragging. This is where you initialize data with dataTransfer.setData().
    • drag: fires repeatedly while the element is being dragged. Useful for real-time visual feedback.
    • dragend: fires when the user releases the mouse button, regardless of whether the element reached a drop target. Ideal for resetting styles.
  • Events on the drop target
    • dragenter: the dragged element enters the target zone. Perfect for visually highlighting the container.
    • dragover: fires continuously while the element is over the target zone. Calling event.preventDefault() here is required so the drop event can fire.
    • dragleave: the dragged element leaves the target zone without dropping. Use it to revert styles applied in dragenter.
    • drop: the element is dropped inside the target zone. This is where you retrieve the data using dataTransfer.getData() and execute business logic.
  • Common mistakes that break DnD
    • Not calling event.preventDefault() in dragover. This blocks the drop event completely. It happened to me so often that adding it became second nature.
    • Modifying the DOM inside dragover. Since this event fires dozens of times per second, any DOM manipulation inside it severely impacts performance.
    • Not resetting styles in dragleave or dragend. The result is drop zones remaining visually "active" even when nothing is being dragged anymore.
    • In Firefox, calling event.dataTransfer.setData() in the dragstart event is mandatory, even with an empty string. Without it, dragging does not work in that browser.

Drag and Drop is a feature that allows "grabbing" an object and dragging it to a different location. In HTML5, this is accomplished entirely natively, without relying on jQuery UI or other libraries.

Basic Drag and Drop example in HTML5

One of the major features added to HTML5 was native support for drag and drop. It can be applied to any DOM element: <div> containers, <textarea> elements, <p> paragraphs, headings, images, etc.

It is a powerful tool that takes user interaction to another level, and it is particularly attractive when combined with touch screens on tablets and smartphones.

An application should be intuitive. And what is more intuitive than dragging an "object" from one place to another?

What exactly is the drag and drop feature?

With Drag and Drop, we simply move an element from A to B by "dragging" it across. The concept isn't exclusive to HTML: technologies like Android or iOS use the exact same idea, which helps users understand it immediately.

However, while the concept is simple, the HTML5 API exposes a significant number of methods, events, and properties that allow you to take full advantage of this technology. Don't worry: you'll explore them one by one through concrete examples.

Getting started with Drag and Drop in HTML5

In this guide, we'll cover key definitions of how this technology works, its components, methods, attributes, and several practical examples that will surely prove useful in day-to-day work.

To create draggable elements, you no longer need external plugins. With HTML5 and vanilla JavaScript, you can implement Drag and Drop on any DOM element.

Drag and Drop events in JavaScript — Drag (draggable element)

The draggable elements trigger three events during their lifecycle:

1.0 dragstart

This event fires the moment the user starts dragging an element. This is where you specify what you're dragging and set the corresponding values using the setData() method. It is invoked only once per drag operation.

Additionally, in this event you can apply a custom visual style to the element while it is being dragged, for example by changing its opacity or adding a CSS class.

2.0 drag

This event fires immediately after dragstart and continues firing while the user keeps holding the element. The exact frequency with which it fires depends on the browser.

This event is useful for tracking the status of the draggable element at all times; for instance, to monitor its exact real-time position.

3.0 dragend

Occurs when the user finishes dragging the element, whether by dropping it into a valid container or simply releasing the mouse button anywhere else. It executes only once and is the ideal spot to reset styles or state.

The HTML attributes corresponding to these events are: ondragstart, ondrag, and ondragend.

Drag and Drop events in JavaScript — Drop (target zone)

The container or drop target triggers four events:

1.0 dragenter

Fires when the dragged element enters the drop zone, but hasn't been dropped yet.

This event is ideal for changing container styles to give the user a visual cue that the element can be dropped here.

In this event, you can also inspect the transferred data (dataTransfer) and the data type initialized via setData().

2.0 dragleave

Fires when a dragged element leaves the target drop zone without being dropped.

This event is perfect for reverting any visual styles applied during dragenter.

3.0 dragover

Fires continuously while the dragged element moves inside the drop target container, and stops only when the element is dropped or leaves the zone. Its firing frequency depends on the browser.

Similar to drag, it's useful for tracking the exact position of the element inside the container. Always remember to call event.preventDefault() here; otherwise, the drop event will never fire.

4.0 drop

Fires when the dragged element is dropped inside the container. In this event, you retrieve the transferred data using the getData() method and execute the relevant logic.

The corresponding HTML attributes are: ondragenter, ondragleave, ondragover, and ondrop.

Drag and Drop attributes in HTML5

Handling events alone isn't enough. To make a DOM element draggable, you must add the attribute draggable="true".

Any DOM element can be made draggable: <div>, <img>, <p>, <li>, etc.

So far, we've seen what events occur during the Drag and Drop lifecycle and how draggable elements interact with their containers. But one key question remains:

How do we send data associated with the element being dragged?

That is: how do we determine what information the element "carries with it" during the drag? The following object handles that.

The dataTransfer object in HTML5 Drag and Drop

Dragging and dropping elements without associated data has limited usefulness. The dataTransfer object acts as the brain of the operation: it allows setting information in the dragstart() event and retrieving it in the drop() event. Let's look at its most important methods:

.setData(format, data)

Use this method to store data from the dragged element. You must specify the data type (MIME format) to ensure cross-browser compatibility:

  • For plain text: "text/plain".
  • For a URL: "text/uri-list".
  • For structured HTML: "text/html".

You should set this data in the dragstart event using event.dataTransfer.setData(type, data).

.getData(format)

Returns the data previously stored with setData(). It can only be retrieved successfully during the drop event, using event.dataTransfer.getData(type).

.clearData()

Clears all data set by setData(format, data) using event.dataTransfer.clearData(type). Useful for resetting state between operations.

setData / getData — quick example

// In dragstart:
event.dataTransfer.setData("text/plain", element.id);

// In drop:
const id = event.dataTransfer.getData("text/plain");

Supported MIME types

  • "text/plain" — unformatted text (most compatible)
  • "text/html" — structured HTML
  • "text/uri-list" — URLs

Custom drag image (ghost image)

By default, the browser displays a semi-transparent copy of the element while dragging. You can replace it with a custom image:

event.dataTransfer.setDragImage(myImage, 10, 10);

The last two parameters define the cursor offset (in pixels) relative to the image.

Step-by-step practical examples (ready-to-use code)

Let's go over a progressive series of examples to help you understand how to use HTML5 Drag and Drop and adapt it to your projects.

1.0 Basic HTML5 Drag and Drop example using the :after pseudo-element

This is the simplest example: it uses the minimal set of events needed for dragging to work. It serves as an ideal starting point before adding complexity.

Complete code (HTML and JavaScript)

function dragstart(box, event) {
    // Store the ID of the dragged element
    event.dataTransfer.setData('Data', box.id);
}
function drop(target, event) {
    // Retrieve the ID stored in dragstart
    var box = event.dataTransfer.getData('Data');
    // Move the element to the new container
    target.appendChild(document.getElementById(box));
}

When dragging begins (dragstart event), we specify which data to transfer via setData(). When the element is dropped into the container, the drop event retrieves that data with getData() and re-inserts the node into the DOM.

You can check the full example at:

As you can see, we use the CSS selector :after to dynamically change the text of the draggable element (the box) depending on whether it is inside or outside the target container.

Customizing the drop zone using CSS

You can provide visual feedback for the drop zone by combining JavaScript events with CSS classes:

.drop-zone { transition: background 0.2s ease; }
.drop-zone.over { background: #e0f7fa; border: 2px dashed #0097a7; }

2.0 Drag and Drop example with all events in action

To better understand how all events interact, this example extends the previous one by registering every event supported by the API and logging a message in the browser console for each one:

Complete JavaScript code

function dragstart(box, event) {
    event.dataTransfer.setData('Data', box.id);
}
function drag(target, event) {
    console.log("drag");
    return false;
}
function dragend(target, event) {
    console.log("dragend");
    return false;
}
function dragenter(target, event) {
    console.log("dragenter");
    return false;
}
function dragleave(target, event) {
    console.log("dragleave");
    return false;
}
function dragover(event) {
    console.log("dragover");
    event.preventDefault(); // Required to allow drop
    return false;
}
function drop(target, event) {
    var box = event.dataTransfer.getData('Data');
    target.appendChild(document.getElementById(box));
}

The core logic is identical to the previous example. The difference is that now you will see multiple events executing in the Developer Console (F12) as you position the draggable element. It's a great way to grasp the order and execution frequency of each event.

You can check the full example at:

3.0 Dragging and dropping files from the desktop

You've likely attached files by dragging them directly into Gmail. With HTML5, this behavior is surprisingly easy to replicate:

File drag and drop in Gmail

The secret lies in the fact that when a drop event occurs with OS files, the event.dataTransfer.files property contains the list of dragged files. From there, you can read them using the FileReader API or upload them directly to the server via fetch or XMLHttpRequest.

Complete JavaScript code

var MAX_BYTES = 102400; // 100 KB

function dragenter(event) {
    event.stopPropagation();
    event.preventDefault();
}
function dragover(event) {
    event.stopPropagation();
    event.preventDefault();
}
function drop(event) {
    console.log('drop', event);
    event.stopPropagation();
    event.preventDefault();

    var data = event.dataTransfer;
    var files = data.files;
    var file;
    var reader;

    for (var i = 0; i < files.length; i++) {
        file = files[i];
        reader = new FileReader();
        reader.onloadend = onFileLoaded;
        reader.readAsBinaryString(file);
    }
}
function onFileLoaded(event) {
    document.getElementById("resultado").value = event.currentTarget.result.substr(0, MAX_BYTES);
}

var container = document.getElementById("contenedor");
container.addEventListener("dragenter", dragenter, false);
container.addEventListener("dragover", dragover, false);
container.addEventListener("drop", drop, false);

The File API in JavaScript is a subject of its own; you can find more details in the link at the top of this article.

You can check the full example at:

4.0 Styling elements dynamically with Drag and Drop events

In this example, we see how to dynamically adjust element styles using each event in the DnD lifecycle. This technique is essential for providing clear visual feedback to the user:

Complete JavaScript code

function dragstart(box, event) {
    document.getElementById(box.id).className = "in";
    event.dataTransfer.setData('Data', box.id);
}
function drag(box, event) {
    return false;
}
function dragend(box, event) {
    document.getElementById(box.id).className = "out";
    return false;
}
function dragenter(target, event) {
    document.getElementById("contenedor").className = "inContainer";
    return false;
}
function dragleave(target, event) {
    document.getElementById("contenedor").className = "outContainer";
    return false;
}
function dragover(event) {
    event.preventDefault();
    return false;
}
function drop(target, event) {
    var box = event.dataTransfer.getData('Data');
    document.getElementById("contenedor").className = "outContainer";
    target.appendChild(document.getElementById(box));
}

You can check the full example at:

5.0 Basic Drag and Drop with ES6+ (Arrow Functions)

The same logic using modern syntax with arrow functions. This pattern is the cleanest choice for new projects:

<div id="origen" draggable="true">A</div>
<div id="destino"></div>

<script>
origen.addEventListener("dragstart", e => {
    e.dataTransfer.setData("text/plain", e.target.id);
});

destino.addEventListener("dragover", e => e.preventDefault());

destino.addEventListener("drop", e => {
    e.preventDefault();
    const id = e.dataTransfer.getData("text/plain");
    destino.appendChild(document.getElementById(id));
});
</script>

6.0 File upload via desktop dragging (modern version)

This is one of the most practical applications. When a drop event fires with a file from the operating system, you can capture it and send it to the server using fetch:

container.addEventListener("dragover", e => e.preventDefault());

container.addEventListener("drop", e => {
    e.preventDefault();
    const files = e.dataTransfer.files;

    for (const file of files) {
        console.log("File:", file.name, "Size:", file.size, "bytes");
        // Validate MIME type and size here before uploading
        // Upload file via fetch or XMLHttpRequest
    }
});

How to improve the user visual experience

  • Dynamic styles using dragenter / dragleave
    • zone.addEventListener("dragenter", () => zone.classList.add("over"));
      zone.addEventListener("dragleave", () => zone.classList.remove("over"));
  • Visual indicators
    • Dashed borders, background highlighting, action icons. Use the .over class to indicate that the zone is ready to receive the element.
  • Microinteractions
    • 100–200 ms CSS transitions make a massive difference in perceived smoothness. Less is more.

Drag and Drop on mobile and touchscreens: limitations and solutions

Here comes one of the most searched topics in Search Console data, and for good reason: native HTML5 DnD does not work the same way on mobile devices.

  • Mobile limitations
    • The drag, dragover, and drop events don't always fire in mobile browsers.
    • The browser-generated ghost image does not appear on iOS/Android.
      • Movement relies on the OS touch system rather than the browser.
    • The attribute draggable="true" is ignored by iOS Safari and many Android browsers.
  • Solutions
    • Combine with native touch events: touchstart, touchmove, touchend.
    • Use specialized libraries that bridge both worlds: SortableJS, InteractJS, Dragula.
  • Maintaining a smooth experience across both environments is key to good design.

In projects where I use Laravel or Django as the backend, I usually combine touch events with native DnD to keep a unified workflow across desktop and mobile without duplicating logic.

To detect if the user is on a touch device and issue a warning or adapt the interface:

const isTouch = 'ontouchstart' in window;
if (isTouch) {
    console.warn("Native DnD has limitations on mobile. Consider using SortableJS.");
}

Optimization and performance in drag operations

Optimization and Performance in Drag Operations

  • Minimize Repaints
    • Avoid modifying styles or the DOM directly inside dragover. Since this event can trigger dozens of times per second, any expensive operation here will ruin the experience.
  • Event Delegation
    • Instead of attaching individual listeners to each draggable element, handle events from a single listener on the parent container. This is especially useful when elements are added dynamically to the DOM.
  • Other Real-World Recommendations
    • Apply throttling to high-frequency callbacks using requestAnimationFrame.
    • Avoid overly deep or heavy DOM trees inside draggable elements.
    • Use CSS transforms (transform: translate()) to move elements visually instead of modifying their position properties, as transforms do not trigger reflows.

Security and Safe Data Handling in DnD

The dataTransfer object is powerful, but there are security considerations you shouldn't ignore:

  • HTML Sanitization
    • If you transfer HTML via dataTransfer and insert it into the DOM using innerHTML, always sanitize the content beforehand. An attacker could inject malicious scripts (XSS).
  • Prevention of Data Leaks
    • Never transfer session tokens, passwords, API keys, or other sensitive data via dataTransfer. Use opaque identifiers or indices instead.
  • DnD and File Uploads
    • Always validate files before processing or sending them to the server:
      • Size: check file.size before uploading.
      • Extension: filter by extension using an allowlist.
      • MIME Type: validate file.type, though keep in mind that the client can tamper with it; final validation must take place on the server.

In my file upload implementations with Laravel, I always validate the size and MIME type on the client side to provide immediate feedback, and re-validate them on the server before persisting anything.

Conclusion

In this guide on Drag and Drop with HTML5, we explored how to build your own drag-and-drop system from scratch, going over the seven lifecycle events, the dataTransfer object, and a series of progressive examples ranging from the most basic use case to uploading files directly from the desktop.

The number of events managed by the API might seem overwhelming at first, but it is actually quite the opposite: that granularity is what gives you total control over the state of both the draggable element and the container at any given moment, allowing you to customize the experience nearly 100%.

Native Drag and Drop in HTML5 and JavaScript remains a powerful, flexible tool without external dependencies. With just a few events, you can build advanced interfaces, from Trello-style boards to file upload modules. And when your project demands it—whether due to needing robust mobile support or complex reordering—you now know when to make the leap to a library.

When should you use a library instead of the native API?

  • Large projects or those with many draggable elements that change dynamically.
  • Complex reordering with insertion animations between elements.
  • Need for mobile/touch support without manually managing touchstart / touchmove events.

Learn now how to copy and paste with vanilla JavaScript using the Clipboard API.

Learn native Drag and Drop with HTML5 and JavaScript: events, dataTransfer object, file upload, mobile support solution, avoid common mistakes with this step-by-step guide and real examples.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.