The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Make a source draggable | <li draggable="true" data-item-id="task-42">
Review draft
</li> | View examples |
| Write a typed payload | event.dataTransfer.setData('application/x-task-id', taskId) | View examples |
| Limit source operations | event.dataTransfer.effectAllowed = 'move' | View examples |
| Set drag feedback | event.dataTransfer.setDragImage(preview, 16, 16) | View examples |
| Accept drag entry | zone.addEventListener('dragenter', event => event.preventDefault()) | View examples |
| Enable dropping | zone.addEventListener('dragover', event => event.preventDefault()) | View examples |
| Show copy feedback | event.dataTransfer.dropEffect = 'copy' | View examples |
| Inspect offered formats | event.dataTransfer.types.includes('text/plain') | View examples |
| Inspect transfer items | [...event.dataTransfer.items].some(item => item.kind === 'file') | View examples |
| Read dropped text | const id = event.dataTransfer.getData('application/x-task-id') | View examples |
| Complete a custom drop | event.preventDefault() | View examples |
| Clean up after dragging | source.addEventListener('dragend', () => source.classList.remove('dragging')) | View examples |
| Offer a button alternative | <button type="button" data-action="move">
Move to Done
</button> | View examples |
HTML drag and drop is an event-driven enhancement for moving or copying data with a pointing device. A source writes a small, typed payload during dragstart; a target advertises acceptance during dragenter and dragover, then consumes the payload during drop. Keep the transferred value narrowly scoped, render dropped text as text rather than markup, and always offer an ordinary control for keyboard, touch, and assistive-technology users.
Step by step
Detailed examples
Write drag data only when the drag starts
Set draggable=true on intentional custom sources and populate dataTransfer in dragstart, the only drag event whose data store is normally writable. Transfer an opaque identifier rather than serialized HTML or sensitive records, and include text/plain only when a meaningful text fallback is appropriate. effectAllowed constrains copy, move, or link semantics; setDragImage changes visual feedback but not the operation itself.
<li id="task-42" draggable="true" data-item-id="task-42">Review draft</li>
<script>
const source = document.querySelector('#task-42');
source.addEventListener('dragstart', event => {
event.dataTransfer.setData('application/x-task-id', source.dataset.itemId);
event.dataTransfer.setData('text/plain', source.textContent.trim());
event.dataTransfer.effectAllowed = 'move';
source.classList.add('dragging');
});
</script> Note: Do not put authentication tokens, private object contents, or trusted HTML in a drag payload. Treat data received from another document as untrusted input.
source.addEventListener('dragstart', event => {
const preview = document.querySelector('#drag-preview');
event.dataTransfer.setDragImage(preview, 16, 16);
}); Explicitly accept a compatible drag
A custom target must cancel dragover or the browser normally rejects the drop. Cancel dragenter as well when the offered types are acceptable, and set dropEffect during dragover to communicate the intended operation. The requested dropEffect must be compatible with the source's effectAllowed; otherwise the resulting operation is none.
const zone = document.querySelector('#done-list');
const taskType = 'application/x-task-id';
zone.addEventListener('dragenter', event => {
if (event.dataTransfer.types.includes(taskType)) event.preventDefault();
});
zone.addEventListener('dragover', event => {
if (!event.dataTransfer.types.includes(taskType)) return;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}); Note: Do not cancel every page-level dragover indiscriminately; doing so can turn unrelated elements into apparent drop targets and interfere with browser behavior.
Inspect formats and items during protected events
DataTransfer.types lists offered string formats and includes Files when files are present. DataTransfer.items exposes each item's kind (string or file) and type. During events such as dragenter and dragover the store is protected: code can enumerate formats and kinds to decide whether to show acceptance, but should not expect getData or files to reveal the underlying payload until drop.
zone.addEventListener('dragenter', event => {
const types = event.dataTransfer.types;
const carriesFiles = types.includes('Files');
const carriesURL = types.includes('text/uri-list');
const carriesText = types.includes('text/plain');
zone.dataset.offer = carriesFiles ? 'files' : carriesURL ? 'url' : carriesText ? 'text' : 'unsupported';
});
zone.addEventListener('dragleave', () => {
delete zone.dataset.offer;
}); const offered = [...event.dataTransfer.items].map(item => ({
kind: item.kind,
type: item.type || '(unknown)'
})); Consume and validate data in the drop handler
The drag data store becomes readable for drop. Cancel the drop event, request the exact type you accepted, validate the value against current application state, and perform the smallest DOM or model update needed. Never insert dropped text with innerHTML, and validate dropped URLs by parsing them and allowing only schemes and destinations your feature actually supports.
zone.addEventListener('drop', event => {
event.preventDefault();
const id = event.dataTransfer.getData('application/x-task-id');
if (!/^task-[a-z0-9-]+$/.test(id)) return;
const task = document.getElementById(id);
if (!task || !task.matches('[data-item-id]')) return;
zone.append(task);
delete zone.dataset.offer;
}); zone.addEventListener('drop', event => {
event.preventDefault();
const text = event.dataTransfer.getData('text/plain').slice(0, 500);
const output = document.querySelector('#dropped-text');
output.textContent = text;
}); Note: A MIME-looking transfer type describes a format; it does not prove that the contents are safe or authentic.
Pair visual drag feedback with ordinary controls
Use dragstart, dragenter, dragleave, drop, and dragend to expose clear transient state, then remove it on every completion path. Nested children can make dragenter and dragleave fire more often than expected, so style from current accepted state rather than assuming one enter/leave pair. Pointer dragging alone is not a complete interaction: provide named buttons, menus, or other keyboard-operable controls that invoke the same model action, announce important results through a status region, and test touch behavior separately.
source.addEventListener('dragend', () => {
source.classList.remove('dragging');
document.querySelectorAll('[data-offer]').forEach(element => {
delete element.dataset.offer;
});
});
function moveTask(task, destination) {
destination.append(task);
document.querySelector('#move-status').textContent = `${task.textContent.trim()} moved.`;
} <li id="task-7">
<span>Publish notes</span>
<button type="button" data-action="move">Move to Done</button>
</li>
<p id="move-status" role="status"></p> Local code tester
Try a movable task
Drag the task to Done, or use the equivalent button. The demo transfers only a validated local identifier and makes no network requests.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



