draggable

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

ValueBehavior
"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.

EventFires OnWhen
dragstartDragged elementUser begins dragging
dragDragged elementContinuously while dragging
dragenterDrop targetDragged item enters the target
dragoverDrop targetContinuously while over the target
dragleaveDrop targetDragged item leaves the target
dropDrop targetItem is dropped on the target
dragendDragged elementDragging 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

TypeUse Case
text/plainSimple text, IDs, labels
text/htmlRich HTML fragments
text/uri-listURLs
application/jsonStructured 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.

<ul id="sortable-list"> <li draggable="true">Item 1</li> <li draggable="true">Item 2</li> <li draggable="true">Item 3</li> <li draggable="true">Item 4</li> </ul> <script> const list = document.getElementById('sortable-list'); let draggedItem = null; list.addEventListener('dragstart', (e) => { draggedItem = e.target; e.target.style.opacity = '0.4'; e.dataTransfer.effectAllowed = 'move'; }); list.addEventListener('dragover', (e) => { e.preventDefault(); const target = e.target.closest('li'); if (target && target !== draggedItem) { const rect = target.getBoundingClientRect(); const midY = rect.top + rect.height / 2; if (e.clientY < midY) { list.insertBefore(draggedItem, target); } else { list.insertBefore(draggedItem, target.nextSibling); } } }); list.addEventListener('dragend', (e) => { e.target.style.opacity = '1'; draggedItem = null; }); </script>
  • Item 1 — Drag to reorder
  • Item 2 — Drag to reorder
  • Item 3 — Drag to reorder
  • Item 4 — Drag to reorder

Example: Drag to Delete

Items can be dragged to a trash zone to remove them. Visual feedback on the drop zone helps the user understand where to drop.

<style> .drop-zone-active { outline: 2px dashed var(--color-danger); background: var(--color-danger-subtle); } </style> <ul id="card-list"> <li draggable="true">Draft blog post</li> <li draggable="true">Meeting notes</li> <li draggable="true">Old todo list</li> </ul> <div id="trash-zone" role="region" aria-label="Drop here to delete"> <p>🗑 Drop here to delete</p> </div> <script> const cards = document.getElementById('card-list'); const trash = document.getElementById('trash-zone'); let draggedCard = null; cards.addEventListener('dragstart', (e) => { draggedCard = e.target; e.dataTransfer.setData('text/plain', e.target.textContent); e.dataTransfer.effectAllowed = 'move'; }); trash.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; trash.classList.add('drop-zone-active'); }); trash.addEventListener('dragleave', () => { trash.classList.remove('drop-zone-active'); }); trash.addEventListener('drop', (e) => { e.preventDefault(); trash.classList.remove('drop-zone-active'); if (draggedCard) { draggedCard.remove(); draggedCard = null; } }); </script>

Accessibility

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
  • Accessibility Guide — overview of VB's accessibility patterns