The Drag and Drop API lets users grab an element with the mouse, move it around, and place it in a designated drop zone. It’s commonly used in file upload areas, customizable dashboards, editors, and interactive UI components.
A complete drag-and-drop interaction involves two main HTML elements:
Draggable element – the item the user picks up and moves.
Drop target – the area where the draggable item can be released.
🚧 Events Fired on Draggable Elements
When an element is dragged, the following events occur in sequence:
dragstart– Triggered as soon as the user clicks and begins dragging the element. The cursor changes to indicate that dropping on itself isn't allowed.drag– Fires continuously while the user is actively dragging the element.dragend– Fires when the user releases the mouse button and the dragging stops.
In all these events, e.target refers to the element being dragged.
The browser does not automatically alter the appearance of the dragged item, so you can style or animate it as needed.
📎 Events Fired on Drop Targets
When the dragged element interacts with a drop zone, events occur in this order:
dragenter– Fires when the dragged item first enters the drop target’s boundaries.dragover– Fires repeatedly as long as the dragged item remains over the drop target.dragleaveordrop–dragleavefires when the dragged item leaves the drop target’s boundaries.dropfires when the user releases the dragged item inside the drop target.
For all these events, e.target refers to the drop target element.
✅ Valid Drop Target
A valid drop target is any HTML element that is allowed to accept a dragged item.
Even though most elements can listen to drop-related events (
dragenter,dragover,dragleave,drop), they DO NOT accept drops by default.If dropping isn’t allowed, the
dropevent will never fire.To make an element a valid drop target, you must:
Listen to the
dragovereventCall
event.preventDefault()inside it (this enables dropping)Optionally handle the
dropevent to process the dropped data
💥 Transferring Data with dataTransfer (Simplified)
During a drag-and-drop operation, data is shared using the
dataTransferobject, available on drag events.It lets you send data from the dragged element → to the drop target.
You use two main methods:
1️⃣ setData() – Store data while dragging
dataTransfer.setData(format, data)format→ e.g.,"text/plain"or"text/uri-list"data→ a string you want to attach to the drag action
Example:
event.dataTransfer.setData("text/plain", "Hello");2️⃣ getData() – Retrieve the data on drop
dataTransfer.getData(format)formatmust match what you used insetData()Returns the string stored earlier, or an empty string if nothing was set
Example:
const text = event.dataTransfer.getData("text/plain");📍Drag and Drop Example
We’ll develop the following simple drag-and-drop application to demonstrate the JavaScript drag and drop API :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta-name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript - Drag and Drop Demo</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<h1>JavaScript - Drag and Drop</h1>
<div class="drop-targets">
<div class="box">
<div class="item" id="item" draggable="true"></div>
</div>
<div class="box"></div>
<div class="box"></div>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 16px;
background-color: #fff;
overflow: hidden;
}
h1 {
color: #323330;
}
.container {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 20px;
}
.drop-targets {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
margin: 20px 0;
}
.box {
height: 150px;
width: 150px;
border: solid 3px #ccc;
margin: 10px;
/* align items in the box */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.drag-over {
border: dashed 3px red;
}
.item {
height: 75px;
width: 75px;
background-color: #F0DB4F;
}
.hide {
display: none;
}// Select draggable element
const item = document.querySelector('.item');
// Enable dragstart
item.addEventListener('dragstart', dragStart);
function dragStart(e) {
e.dataTransfer.setData('text/plain', e.target.id);
// hide the element visually but keep space
setTimeout(() => {
e.target.classList.add('hide');
}, 0);
}
// Select all drop zones
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
box.addEventListener('dragenter', dragEnter);
box.addEventListener('dragover', dragOver);
box.addEventListener('dragleave', dragLeave);
box.addEventListener('drop', drop);
});
function dragEnter(e) {
e.preventDefault();
// highlight ONLY the box, not child elements
if (e.target.classList.contains('box')) {
e.target.classList.add('drag-over');
}
}
function dragOver(e) {
e.preventDefault();
if (e.target.classList.contains('box')) {
e.target.classList.add('drag-over');
}
}
function dragLeave(e) {
if (e.target.classList.contains('box')) {
e.target.classList.remove('drag-over');
}
}
function drop(e) {
if (e.target.classList.contains('box')) {
e.target.classList.remove('drag-over');
// Retrieve dragged item
const id = e.dataTransfer.getData('text/plain');
const draggable = document.getElementById(id);
// Append draggable to new box
e.target.appendChild(draggable);
// Reveal it again
draggable.classList.remove('hide');
}
}<div class="infographic-dragdrop">
<style>
.infographic-dragdrop {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--js-yellow: #F0DB4F;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-dragdrop[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
}
.infographic-dragdrop * { box-sizing: border-box; }
.infographic-dragdrop h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-dragdrop p { margin: 0; }
.infographic-dragdrop button { font-family: inherit; }
.dd-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.dd-icon svg { width: 100%; height: 100%; display: block; }
.dd-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.dd-hero { text-align: center; margin-bottom: 1.3rem; }
.dd-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.dd-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
/* --- demo --- */
.dd-demo-row { display: flex; justify-content: center; margin-bottom: 0.6rem; }
.dd-boxes { display: flex; gap: 1rem; }
.dd-box { height: 110px; width: 110px; border: 3px solid var(--line); border-radius: 8px;
display: flex; align-items: center; justify-content: center; transition: border-color 0.15s ease, background 0.15s ease; }
.dd-box.is-over { border: 3px dashed var(--accent); background: var(--accent-soft); }
.dd-item { height: 55px; width: 55px; background: var(--js-yellow); border-radius: 4px; cursor: grab; }
.dd-item.is-hidden { display: none; }
.dd-live-badge { text-align: center; font-family: var(--mono-font); font-size: 0.68rem; color: var(--ink-soft);
min-height: 1.2em; margin-bottom: 1.2rem; }
.dd-live-badge .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
margin-right: 0.3rem; opacity: 0; }
.dd-live-badge.is-active .dot { opacity: 1; animation: dd-pulse 0.6s ease infinite; }
@keyframes dd-pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.6); opacity: 0.4; } }
/* --- console --- */
.dd-console-panel { max-width: 520px; margin: 0 auto 1.2rem; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.dd-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.dd-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.dd-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.dd-console { min-height: 7rem; max-height: 9.5rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.dd-console-line { font-family: var(--mono-font); font-size: 0.68rem; color: #d4d4d4; padding: 0.08rem 0; animation: dd-pop-in 0.2s ease; white-space: pre-wrap; }
.dd-console-line.is-fn { color: #dcdcaa; }
.dd-console-line.is-detail { color: #9fd9a8; padding-left: 0.9rem; }
.dd-console-empty { font-family: var(--mono-font); font-size: 0.68rem; color: #6b6b6b; }
@keyframes dd-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
/* --- legend --- */
.dd-legend { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; max-width: 620px; margin: 0 auto; }
.dd-chip { display: flex; align-items: center; gap: 0.35rem; border: 1px solid var(--line); border-radius: 20px;
padding: 0.32rem 0.7rem; font-family: var(--mono-font); font-size: 0.66rem; color: var(--ink-soft);
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease; }
.dd-chip .dd-icon { width: 12px; height: 12px; }
.dd-chip.is-firing { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 700; }
.dd-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.dd-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-dragdrop *, .infographic-dragdrop *::before, .infographic-dragdrop *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="dd-wrap">
<div class="dd-demo-row">
<div class="dd-boxes" id="ddBoxes">
<div class="dd-box" data-box="1"><div class="dd-item" id="ddItem" draggable="true"></div></div>
<div class="dd-box" data-box="2"></div>
<div class="dd-box" data-box="3"></div>
</div>
</div>
<div class="dd-live-badge" id="ddLiveBadge"><span class="dot"></span><span id="ddLiveText">idle</span></div>
<div class="dd-console-panel">
<div class="dd-console-head">
<span>>_ event log</span>
<button class="dd-clear-btn" id="ddClearBtn" type="button">clear</button>
</div>
<div class="dd-console" id="ddConsole">
<div class="dd-console-empty">// pick up the tile to begin</div>
</div>
</div>
<div class="dd-legend" id="ddLegend">
<span class="dd-chip" data-evt="dragstart"><span class="dd-icon" id="ddChipIcon0"></span>dragstart</span>
<span class="dd-chip" data-evt="dragenter"><span class="dd-icon" id="ddChipIcon1"></span>dragenter</span>
<span class="dd-chip" data-evt="dragover"><span class="dd-icon" id="ddChipIcon2"></span>dragover</span>
<span class="dd-chip" data-evt="dragleave"><span class="dd-icon" id="ddChipIcon3"></span>dragleave</span>
<span class="dd-chip" data-evt="drop"><span class="dd-icon" id="ddChipIcon4"></span>drop</span>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-dragdrop')
|| document.querySelector('.infographic-dragdrop');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const ICONS = {
move: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="m15 19-3 3-3-3"/><path d="m19 9 3 3-3 3"/><path d="M2 12h20"/><path d="m5 9-3 3 3 3"/><path d="m9 5 3-3 3 3"/></svg>',
login: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/></svg>',
repeat: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>',
logout: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></svg>',
check: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/></svg>'
};
wrapper.querySelector('#ddChipIcon0').innerHTML = ICONS.move;
wrapper.querySelector('#ddChipIcon1').innerHTML = ICONS.login;
wrapper.querySelector('#ddChipIcon2').innerHTML = ICONS.repeat;
wrapper.querySelector('#ddChipIcon3').innerHTML = ICONS.logout;
wrapper.querySelector('#ddChipIcon4').innerHTML = ICONS.check;
const item = wrapper.querySelector('#ddItem');
const boxes = wrapper.querySelectorAll('.dd-box');
const consoleEl = wrapper.querySelector('#ddConsole');
const clearBtn = wrapper.querySelector('#ddClearBtn');
const liveBadge = wrapper.querySelector('#ddLiveBadge');
const liveText = wrapper.querySelector('#ddLiveText');
const chips = wrapper.querySelectorAll('.dd-chip');
function logLine(fnName, detail) {
if (consoleEl.querySelector('.dd-console-empty')) consoleEl.innerHTML = '';
const l1 = document.createElement('div');
l1.className = 'dd-console-line is-fn';
l1.textContent = fnName + '()';
consoleEl.appendChild(l1);
if (detail) {
const l2 = document.createElement('div');
l2.className = 'dd-console-line is-detail';
l2.textContent = detail;
consoleEl.appendChild(l2);
}
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function flashChip(evt) {
const chip = wrapper.querySelector('.dd-chip[data-evt="' + evt + '"]');
if (!chip) return;
chip.classList.add('is-firing');
clearTimeout(chip._t);
chip._t = setTimeout(() => chip.classList.remove('is-firing'), 450);
}
let overThrottle = null;
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.id);
setTimeout(() => item.classList.add('is-hidden'), 0);
logLine('dragStart', "dataTransfer.setData('text/plain', '" + e.target.id + "')");
flashChip('dragstart');
liveBadge.classList.add('is-active');
liveText.textContent = 'dragging\u2026';
});
item.addEventListener('dragend', () => {
liveBadge.classList.remove('is-active');
liveText.textContent = 'idle';
});
boxes.forEach(box => {
box.addEventListener('dragenter', (e) => {
e.preventDefault();
if (e.target.classList.contains('dd-box')) {
e.target.classList.add('is-over');
logLine('dragEnter', 'box ' + e.target.dataset.box + ' \u2014 .drag-over added');
flashChip('dragenter');
}
});
box.addEventListener('dragover', (e) => {
e.preventDefault();
if (e.target.classList.contains('dd-box')) {
e.target.classList.add('is-over');
flashChip('dragover');
clearTimeout(overThrottle);
overThrottle = setTimeout(() => {}, 50);
}
});
box.addEventListener('dragleave', (e) => {
if (e.target.classList.contains('dd-box')) {
e.target.classList.remove('is-over');
logLine('dragLeave', 'box ' + e.target.dataset.box + ' \u2014 .drag-over removed');
flashChip('dragleave');
}
});
box.addEventListener('drop', (e) => {
if (e.target.classList.contains('dd-box')) {
e.target.classList.remove('is-over');
const id = e.dataTransfer.getData('text/plain');
const draggable = wrapper.querySelector('#' + id);
e.target.appendChild(draggable);
draggable.classList.remove('is-hidden');
logLine('drop', 'appended to box ' + e.target.dataset.box + ', tile revealed');
flashChip('drop');
liveBadge.classList.remove('is-active');
liveText.textContent = 'idle';
}
});
});
clearBtn.addEventListener('click', () => {
consoleEl.innerHTML = '<div class="dd-console-empty">// pick up the tile to begin</div>';
});
})();
</script>