Enable native drag-and-drop on any element via the HTML Drag and Drop API. Images and links are draggable by default.
Overview
The draggable attribute enables native drag-and-drop on any HTML element using the browser's built-in Drag and Drop API. When set to "true", the user can click and drag the element to a valid drop target.
Drag-and-drop requires JavaScript to handle the events — the draggable attribute only makes the element draggable. Drop targets need event listeners to accept dropped items.
Values
Value
Behavior
"true"
Element can be dragged by the user
"false"
Element cannot be dragged (the default for most elements)
Default draggable elements: Images (<img>) and links (<a href>) are draggable by default without the attribute. Set draggable="false" to prevent this.
<!-- Make a div draggable -->
<div draggable="true">Drag me</div>
<!-- Prevent an image from being dragged (images are draggable by default) -->
<img src="logo.png" alt="Logo" draggable="false" />
<!-- Prevent a link from being dragged (links are draggable by default) -->
<a href="/home" draggable="false">Home</a>
The Drag-and-Drop Event Lifecycle
The Drag and Drop API fires a sequence of events across the dragged element and the drop target. Understanding this lifecycle is essential for implementing drag-and-drop.
Event
Fires On
When
dragstart
Dragged element
User begins dragging
drag
Dragged element
Continuously while dragging
dragenter
Drop target
Dragged item enters the target
dragover
Drop target
Continuously while over the target
dragleave
Drop target
Dragged item leaves the target
drop
Drop target
Item is dropped on the target
dragend
Dragged element
Dragging ends (drop or cancel)
Critical: You must call e.preventDefault() in the dragover event handler. By default, the browser does not allow drops — preventing the default enables it.
<div id="source" draggable="true">Drag me</div>
<div id="target">Drop here</div>
<script>
const source = document.getElementById('source');
const target = document.getElementById('target');
// 1. dragstart — fired on the dragged element when dragging begins
source.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', source.textContent);
e.dataTransfer.effectAllowed = 'move';
});
// 2. dragenter — fired on a drop target when a dragged item enters it
target.addEventListener('dragenter', (e) => {
e.preventDefault();
target.classList.add('drag-over');
});
// 3. dragover — fired continuously while a dragged item is over a target
// You MUST call preventDefault() to allow dropping
target.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
// 4. dragleave — fired when a dragged item leaves a drop target
target.addEventListener('dragleave', () => {
target.classList.remove('drag-over');
});
// 5. drop — fired when the item is dropped on a valid target
target.addEventListener('drop', (e) => {
e.preventDefault();
target.classList.remove('drag-over');
const data = e.dataTransfer.getData('text/plain');
target.textContent = data;
});
// 6. dragend — fired on the dragged element when dragging ends (drop or cancel)
source.addEventListener('dragend', () => {
// Clean up any drag state
});
</script>
DataTransfer API
The dataTransfer object on drag events carries data from the drag source to the drop target. You can set multiple MIME types so different targets can read the format they understand.
// Setting data during dragstart
element.addEventListener('dragstart', (e) => {
// Set multiple formats — the drop target picks the one it understands
e.dataTransfer.setData('text/plain', 'Simple text');
e.dataTransfer.setData('text/html', '<strong>Rich text</strong>');
e.dataTransfer.setData('application/json', JSON.stringify({ id: 42 }));
// Control the drag cursor
e.dataTransfer.effectAllowed = 'copyMove'; // 'copy', 'move', 'link', 'all', etc.
// Custom drag image
const ghost = document.createElement('div');
ghost.textContent = 'Moving...';
document.body.appendChild(ghost);
e.dataTransfer.setDragImage(ghost, 0, 0);
setTimeout(() => ghost.remove(), 0);
});
// Reading data during drop
target.addEventListener('drop', (e) => {
e.preventDefault();
const text = e.dataTransfer.getData('text/plain');
const json = JSON.parse(e.dataTransfer.getData('application/json'));
const files = e.dataTransfer.files; // Dropped files from the OS
});
Common MIME Types
Type
Use Case
text/plain
Simple text, IDs, labels
text/html
Rich HTML fragments
text/uri-list
URLs
application/json
Structured data (custom)
Example: Reorder List
A sortable list where items can be reordered by dragging. The dragover handler calculates whether to insert before or after the target based on cursor position.
Drag-and-drop is mouse-only by default. The native Drag and Drop API does not work with keyboard or touch (touch support varies across browsers and devices). This is the single biggest limitation of the API.
To make drag-and-drop interfaces accessible, always provide keyboard alternatives:
Reorder lists: Add "Move up" and "Move down" buttons (visually hidden or alongside each item)
Drag to delete: Add a "Delete" button for each item
Drag between zones: Provide a <select> or button to move items between containers
Use aria-grabbed and aria-dropeffect: These ARIA attributes communicate drag state to screen readers (note: they are deprecated in ARIA 1.1, but no replacement exists yet)
Live regions: Announce drag actions to screen readers using aria-live="polite" regions
A pattern that works for mouse users but fails for keyboard-only users violates WCAG 2.1.1 (Keyboard). The keyboard alternative does not need to be drag-and-drop — it just needs to achieve the same result.
Limitations
No keyboard support: The native API is pointer-only. Always provide a keyboard alternative.
Touch inconsistency: Mobile browsers handle drag events differently. For mobile drag-and-drop, consider the Pointer Events API or a dedicated library.
Styling constraints: You cannot fully style the drag ghost image via CSS. Use dataTransfer.setDragImage() for custom visuals.
No drop animation: The browser does not animate the drop. You must implement visual transitions yourself.
Cross-origin restrictions: Data set via dataTransfer.setData() is not readable during dragover for security reasons — only during drop.
See Also
<drag-surface> — accessible drag-and-drop reorder with keyboard support
hidden — hide elements removed by drag-to-delete actions
tabindex — manage focus for keyboard alternatives to drag-and-drop