Skip to content

The draggable Attribute

Global Attribute

The draggable attribute specifies whether an element can be dragged using native browser drag-and-drop functionality. Combined with the Drag and Drop API, it enables building intuitive drag-and-drop interfaces for sorting, uploading files, and moving content.

<element draggable="value">Content</element>
ValueDescription
trueElement can be dragged
falseElement cannot be dragged (default for most elements)
autoBrowser determines if element is draggable (default for images and links)
Result
Result
Result
Result

The drag-and-drop operation involves these events:

EventTargetDescription
dragstartDragged elementUser starts dragging
dragDragged elementElement is being dragged
dragenterDrop targetDragged element enters drop zone
dragoverDrop targetDragged element is over drop zone
dragleaveDrop targetDragged element leaves drop zone
dropDrop targetElement is dropped
dragendDragged elementDrag operation ends
// On the draggable element
element.addEventListener('dragstart', (e) => {
// Set data to transfer
e.dataTransfer.setData('text/plain', element.id);
e.dataTransfer.effectAllowed = 'move';
// Optional: Set custom drag image
const img = new Image();
img.src = 'drag-icon.png';
e.dataTransfer.setDragImage(img, 0, 0);
});
element.addEventListener('dragend', (e) => {
// Clean up after drag
element.classList.remove('dragging');
});
// On the drop target
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // Required to allow drop
e.dataTransfer.dropEffect = 'move';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
const draggedElement = document.getElementById(id);
dropZone.appendChild(draggedElement);
});

The dataTransfer object stores drag data:

element.addEventListener('dragstart', (e) => {
// Set different data types
e.dataTransfer.setData('text/plain', 'Plain text');
e.dataTransfer.setData('text/html', '<strong>HTML</strong>');
e.dataTransfer.setData('application/json', JSON.stringify({id: 123}));
});
dropZone.addEventListener('drop', (e) => {
const text = e.dataTransfer.getData('text/plain');
const html = e.dataTransfer.getData('text/html');
const json = JSON.parse(e.dataTransfer.getData('application/json'));
});
e.dataTransfer.effectAllowed = 'move'; // Only move
e.dataTransfer.effectAllowed = 'copy'; // Only copy
e.dataTransfer.effectAllowed = 'link'; // Only link
e.dataTransfer.effectAllowed = 'all'; // Any effect
e.dataTransfer.dropEffect = 'move'; // Visual feedback for move
e.dataTransfer.dropEffect = 'copy'; // Show copy cursor
e.dataTransfer.dropEffect = 'link'; // Show link cursor
e.dataTransfer.dropEffect = 'none'; // Show no-drop cursor

Make drag operations obvious:

/* While dragging */
.dragging {
opacity: 0.5;
transform: rotate(5deg);
cursor: grabbing;
}
/* Drag handle */
.drag-handle {
cursor: grab;
}
/* Drop zone states */
.dropzone {
transition: all 0.2s;
}
.dropzone.over {
border-color: #3b82f6;
background: #dbeafe;
}

Always prevent default on dragover and drop:

dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // Required!
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault(); // Required!
});

Drag and drop doesn’t work on touch devices by default:

// Use a polyfill or library
// Or implement touch event handlers
element.addEventListener('touchstart', handleTouchStart);
element.addEventListener('touchmove', handleTouchMove);
element.addEventListener('touchend', handleTouchEnd);

Use visual cues for draggable elements:

<div class="card" draggable="true">
<span class="drag-handle">⋮⋮</span>
<span class="content">Drag me</span>
</div>

Drag and drop is mouse-only; provide keyboard alternatives:

<div draggable="true">
Item 1
<button onclick="moveUp()"></button>
<button onclick="moveDown()"></button>
</div>
const trash = document.getElementById('trash');
trash.addEventListener('dragover', (e) => {
e.preventDefault();
trash.classList.add('active');
});
trash.addEventListener('drop', (e) => {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
document.getElementById(id).remove();
trash.classList.remove('active');
});
element.addEventListener('dragstart', (e) => {
// Hold Ctrl/Cmd to copy instead of move
if (e.ctrlKey || e.metaKey) {
e.dataTransfer.effectAllowed = 'copy';
} else {
e.dataTransfer.effectAllowed = 'move';
}
});
// Stop event propagation for nested zones
innerZone.addEventListener('drop', (e) => {
e.stopPropagation(); // Prevent parent from handling
});

Add appropriate ARIA attributes:

<div
draggable="true"
role="button"
aria-grabbed="false"
tabindex="0">
Draggable item
</div>
<div class="item" draggable="true">
<span>Task item</span>
<button aria-label="Move up"></button>
<button aria-label="Move down"></button>
</div>
// Announce changes
const announcement = document.getElementById('aria-live');
announcement.textContent = 'Item moved to Done column';

Excellent support across modern browsers:

FeatureChromeFirefoxSafariEdge
Basic drag and drop4+3.5+3.1+12+
DataTransfer APIAllAllAllAll
File drag and drop7+3.6+6+12+
  • contenteditable - Editable content can be made draggable
  • tabindex - Make non-interactive elements focusable for keyboard access

For production apps, consider using libraries: