HTML5 Mastery: The Complete Web Foundation
HomeInsightsCoursesHTMLDrag and Drop API
HTML5 APIs

Drag & Drop API

Build interactive drag-and-drop interfaces with the HTML5 Drag and Drop API. Learn how to create file uploaders, sortable lists, and draggable UI elements.

What is the Drag & Drop API?

The Drag and Drop API allows users to drag elements and drop them onto other elements, enabling intuitive interactions like file uploads, reordering lists, and moving items between containers.

🎯 Native Support

Built into HTML5, no libraries needed

📁 File Drag

Users can drag files from desktop

🔄 Reordering

Sortable lists and kanban boards

🎨 Customizable

Full control over drag feedback

Basic Drag & Drop

Making Elements Draggable

HTML
<!DOCTYPE html>
<html>
<head>
    <style>
        .draggable {
            padding: 20px;
            background: #4CAF50;
            color: white;
            cursor: move;
            margin: 10px;
        }
        .dropzone {
            min-height: 200px;
            padding: 20px;
            border: 3px dashed #ccc;
            margin: 10px;
        }
        .dropzone.dragover {
            background: #e8f5e9;
            border-color: #4CAF50;
        }
    </style>
</head>
<body>
    <!-- Draggable element --&gt;
    <div class="draggable" draggable="true" ondragstart="handleDragStart(event)">
        Drag me!
    </div>
    
    <!-- Drop zone --&gt;
    <div class="dropzone" 
         ondrop="handleDrop(event)" 
         ondragover="handleDragOver(event)"
         ondragleave="handleDragLeave(event)">
        Drop here
    </div>

    <script>
    function handleDragStart(e) {
        // Store dragged element's data
        e.dataTransfer.setData('text/plain', e.target.textContent);
        e.dataTransfer.effectAllowed = 'move';
    }

    function handleDragOver(e) {
        e.preventDefault(); // Allow drop
        e.currentTarget.classList.add('dragover');
        e.dataTransfer.dropEffect = 'move';
    }

    function handleDragLeave(e) {
        e.currentTarget.classList.remove('dragover');
    }

    function handleDrop(e) {
        e.preventDefault();
        e.currentTarget.classList.remove('dragover');
        
        // Get dragged data
        const data = e.dataTransfer.getData('text/plain');
        e.currentTarget.textContent = 'Dropped: ' + data;
    }
    </script>
</body>
</html>

Drag Events

Events on Draggable Element

  • dragstart - User starts dragging
  • drag - Element is being dragged (fires continuously)
  • dragend - Drag operation ends

Events on Drop Target

  • dragenter - Dragged element enters drop zone
  • dragover - Dragged element is over drop zone (fires continuously)
  • dragleave - Dragged element leaves drop zone
  • drop - Element is dropped
JAVASCRIPT
const draggable = document.getElementById('draggable');
const dropzone = document.getElementById('dropzone');

// Draggable element events
draggable.addEventListener('dragstart', (e) => {
    console.log('Drag started');
    e.dataTransfer.setData('text/plain', 'data');
});

draggable.addEventListener('drag', (e) => {
    // Fires continuously while dragging
});

draggable.addEventListener('dragend', (e) => {
    console.log('Drag ended');
});

// Drop zone events
dropzone.addEventListener('dragenter', (e) => {
    console.log('Entered drop zone');
    e.currentTarget.classList.add('drag-enter');
});

dropzone.addEventListener('dragover', (e) => {
    e.preventDefault(); // REQUIRED to allow drop!
});

dropzone.addEventListener('dragleave', (e) => {
    console.log('Left drop zone');
    e.currentTarget.classList.remove('drag-enter');
});

dropzone.addEventListener('drop', (e) => {
    e.preventDefault();
    console.log('Dropped!');
    const data = e.dataTransfer.getData('text/plain');
});

dataTransfer Object

Setting Data

JAVASCRIPT
element.addEventListener('dragstart', (e) => {
    // Store different data types
    e.dataTransfer.setData('text/plain', 'Simple text');
    e.dataTransfer.setData('text/html', '<strong>HTML</strong>');
    e.dataTransfer.setData('application/json', JSON.stringify({ id: 123 }));
    
    // Set drag effect
    e.dataTransfer.effectAllowed = 'move'; // or 'copy', 'link', 'copyMove', 'all'
});

Getting Data

JAVASCRIPT
dropzone.addEventListener('drop', (e) => {
    e.preventDefault();
    
    // Get data (must match MIME type used in setData)
    const text = e.dataTransfer.getData('text/plain');
    const html = e.dataTransfer.getData('text/html');
    const json = JSON.parse(e.dataTransfer.getData('application/json'));
    
    console.log('Dropped:', text);
});

Setting Drag Image

JAVASCRIPT
element.addEventListener('dragstart', (e) => {
    // Use custom drag image
    const dragImage = document.createElement('div');
    dragImage.textContent = '📦 Dragging...';
    dragImage.style.position = 'absolute';
    dragImage.style.top = '-9999px';
    document.body.appendChild(dragImage);
    
    e.dataTransfer.setDragImage(dragImage, 0, 0);
    
    // Clean up after drag
    setTimeout(() => document.body.removeChild(dragImage), 0);
});

Example: Sortable List

HTML
<!DOCTYPE html>
<html>
<head>
    <style>
        .sortable-list { list-style: none; padding: 0; }
        .sortable-item {
            padding: 15px;
            margin: 5px 0;
            background: #f5f5f5;
            border: 2px solid #ddd;
            cursor: move;
            user-select: none;
        }
        .sortable-item.dragging {
            opacity: 0.5;
        }
        .sortable-item.drag-over {
            border-top: 3px solid #4CAF50;
        }
    </style>
</head>
<body>
    <h2>Sortable List (Drag to Reorder)</h2>
    <ul class="sortable-list" id="list">
        <li class="sortable-item" draggable="true">Item 1</li>
        <li class="sortable-item" draggable="true">Item 2</li>
        <li class="sortable-item" draggable="true">Item 3</li>
        <li class="sortable-item" draggable="true">Item 4</li>
    </ul>

    <script>
    let draggedElement = null;

    const items = document.querySelectorAll('.sortable-item');
    
    items.forEach(item => {
        item.addEventListener('dragstart', function(e) {
            draggedElement = this;
            this.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        });
        
        item.addEventListener('dragend', function() {
            this.classList.remove('dragging');
            
            // Remove all drag-over classes
            items.forEach(item => item.classList.remove('drag-over'));
        });
        
        item.addEventListener('dragover', function(e) {
            e.preventDefault();
            
            if (this === draggedElement) return;
            
            // Get mouse position relative to item
            const rect = this.getBoundingClientRect();
            const midpoint = rect.top + rect.height / 2;
            
            // Insert before or after based on position
            if (e.clientY < midpoint) {
                this.parentNode.insertBefore(draggedElement, this);
            } else {
                this.parentNode.insertBefore(draggedElement, this.nextSibling);
            }
        });
        
        item.addEventListener('dragenter', function(e) {
            if (this !== draggedElement) {
                this.classList.add('drag-over');
            }
        });
        
        item.addEventListener('dragleave', function() {
            this.classList.remove('drag-over');
        });
    });
    </script>
</body>
</html>

Example: File Drop Upload

HTML
<!DOCTYPE html>
<html>
<head>
    <style>
        .drop-zone {
            width: 400px;
            height: 200px;
            border: 3px dashed #ccc;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 18px;
            color: #666;
            transition: all 0.3s;
        }
        .drop-zone.dragover {
            background: #e3f2fd;
            border-color: #2196F3;
            color: #2196F3;
        }
        .file-list {
            margin-top: 20px;
        }
        .file-item {
            padding: 10px;
            margin: 5px 0;
            background: #f5f5f5;
            border-left: 4px solid #4CAF50;
        }
    </style>
</head>
<body>
    <div class="drop-zone" id="dropZone">
        Drop files here or click to upload
    </div>
    <div class="file-list" id="fileList"></div>

    <script>
    const dropZone = document.getElementById('dropZone');
    const fileList = document.getElementById('fileList');

    // Prevent default drag behaviors on document
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        document.body.addEventListener(eventName, preventDefaults, false);
    });

    function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
    }

    // Highlight drop zone when dragging
    ['dragenter', 'dragover'].forEach(eventName => {
        dropZone.addEventListener(eventName, () => {
            dropZone.classList.add('dragover');
        });
    });

    ['dragleave', 'drop'].forEach(eventName => {
        dropZone.addEventListener(eventName, () => {
            dropZone.classList.remove('dragover');
        });
    });

    // Handle dropped files
    dropZone.addEventListener('drop', (e) => {
        const files = e.dataTransfer.files;
        handleFiles(files);
    });

    // Also allow click to upload
    dropZone.addEventListener('click', () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.multiple = true;
        input.onchange = (e) => handleFiles(e.target.files);
        input.click();
    });

    function handleFiles(files) {
        fileList.innerHTML = '';
        
        [...files].forEach(file => {
            const div = document.createElement('div');
            div.className = 'file-item';
            div.innerHTML = `
                <strong>${file.name}</strong>
                <br>
                Size: ${formatFileSize(file.size)}
                <br>
                Type: ${file.type || 'unknown'}
            `;
            fileList.appendChild(div);
            
            // Read file content (for images)
            if (file.type.startsWith('image/')) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const img = document.createElement('img');
                    img.src = e.target.result;
                    img.style.maxWidth = '200px';
                    img.style.marginTop = '10px';
                    div.appendChild(img);
                };
                reader.readAsDataURL(file);
            }
        });
    }

    function formatFileSize(bytes) {
        if (bytes === 0) return '0 Bytes';
        const k = 1024;
        const sizes = ['Bytes', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
    }
    </script>
</body>
</html>

Example: Kanban Board

HTML
<!DOCTYPE html>
<html>
<head>
    <style>
        .board {
            display: flex;
            gap: 20px;
            padding: 20px;
        }
        .column {
            flex: 1;
            background: #f5f5f5;
            padding: 15px;
            border-radius: 8px;
            min-height: 400px;
        }
        .column h3 {
            margin-top: 0;
            color: #666;
        }
        .card {
            background: white;
            padding: 15px;
            margin: 10px 0;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            cursor: move;
        }
        .card.dragging {
            opacity: 0.5;
        }
        .column.drag-over {
            background: #e8f5e9;
            border: 2px dashed #4CAF50;
        }
    </style>
</head>
<body>
    <div class="board">
        <div class="column" data-status="todo">
            <h3>To Do</h3>
            <div class="card" draggable="true">Task 1</div>
            <div class="card" draggable="true">Task 2</div>
        </div>
        
        <div class="column" data-status="inprogress">
            <h3>In Progress</h3>
            <div class="card" draggable="true">Task 3</div>
        </div>
        
        <div class="column" data-status="done">
            <h3>Done</h3>
            <div class="card" draggable="true">Task 4</div>
        </div>
    </div>

    <script>
    let draggedCard = null;

    // Make cards draggable
    function setupCard(card) {
        card.addEventListener('dragstart', function(e) {
            draggedCard = this;
            this.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        });
        
        card.addEventListener('dragend', function() {
            this.classList.remove('dragging');
        });
    }

    // Make columns drop targets
    function setupColumn(column) {
        column.addEventListener('dragover', function(e) {
            e.preventDefault();
            this.classList.add('drag-over');
        });
        
        column.addEventListener('dragleave', function(e) {
            // Only remove if leaving column entirely
            if (e.target === this) {
                this.classList.remove('drag-over');
            }
        });
        
        column.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('drag-over');
            
            if (draggedCard) {
                this.appendChild(draggedCard);
                
                // Log status change
                const newStatus = this.dataset.status;
                console.log('Card moved to:', newStatus);
            }
        });
    }

    // Initialize
    document.querySelectorAll('.card').forEach(setupCard);
    document.querySelectorAll('.column').forEach(setupColumn);
    </script>
</body>
</html>

Mobile Touch Support

Native Drag & Drop API doesn't work on mobile. For mobile support, you need to handle touch events:

JAVASCRIPT
// Simple touch drag support
let touchDragging = false;
let touchElement = null;

element.addEventListener('touchstart', (e) => {
    touchDragging = true;
    touchElement = e.target;
    // Add visual feedback
    touchElement.classList.add('dragging');
});

element.addEventListener('touchmove', (e) => {
    if (!touchDragging) return;
    e.preventDefault();
    
    // Move element with touch
    const touch = e.touches[0];
    touchElement.style.position = 'absolute';
    touchElement.style.left = touch.pageX + 'px';
    touchElement.style.top = touch.pageY + 'px';
});

element.addEventListener('touchend', (e) => {
    touchDragging = false;
    touchElement.classList.remove('dragging');
    
    // Check drop target
    const dropTarget = document.elementFromPoint(
        e.changedTouches[0].clientX,
        e.changedTouches[0].clientY
    );
    
    // Handle drop
    if (dropTarget && dropTarget.classList.contains('dropzone')) {
        dropTarget.appendChild(touchElement);
    }
});

// Note: For production, consider using libraries like:
// - SortableJS (touch support included)
// - react-beautiful-dnd
// - interact.js

Best Practices

✅ Do

  • Always call e.preventDefault() in dragover
  • Provide visual feedback during drag
  • Use appropriate cursor (cursor: move)
  • Handle dragend to clean up state
  • Test keyboard accessibility
  • Consider mobile/touch support
  • Add ARIA attributes for screen readers

❌ Don't

  • Forget preventDefault() in dragover
  • Make everything draggable (confusing UX)
  • Ignore mobile users
  • Forget to clean up event listeners
  • Assume drag & drop is intuitive (provide hints)

Browser Support

Drag & Drop API is supported in all modern browsers:

  • ✅ Chrome 4+
  • ✅ Firefox 3.5+
  • ✅ Safari 3.1+
  • ✅ Edge (all versions)
  • ⚠️ Mobile: Limited native support (use touch events or libraries)

What's Next?

Learn about Web Workers for running JavaScript in background threads.