drag & drop

File Upload & Drag-Drop — Live Demos

5 interactive examples

The dragleave flickering bug — and the counter fix

Both zones have child elements inside. Drag a file (or any content) over each. Left zone flickers on child elements. Right uses a counter — highlight stays solid.

❌ Flickering — naive dragleave
📂

Drop here

Child elements cause flicker

icon text badge
✅ Counter fix — no flicker
📂

Drop here or browse

Counter tracks enter/leave nesting

icon text badge
--:--:--Drag any file over both zones — watch for flickering in the left zone

Mobile warning — drag-drop doesn't work on touch devices

⚠ The HTML Drag and Drop API uses mouse events. Touchscreen input (phones, tablets) does NOT trigger drag events. The <input type="file"> click trigger is the primary interface for mobile users — not a fallback. Always provide it.

Multi-file drop with full validation

Drop multiple files. Validates file count (max 5), allowed types (images only), and individual size (max 2MB). All errors shown in the announcement area.

🖼

Drop up to 5 images

PNG, JPG, WebP, or GIF · Max 2MB each · Up to 5 files

--:--:--Drop image files to test validation (try dropping non-images or large files)

Upload progress with XHR — Fetch can't do this

Select a file to simulate an upload with a real progress bar. Uses XMLHttpRequest.upload.onprogress — the Fetch API has no equivalent. Uses the semantic <progress> element.

Select a file

Simulates upload with real XHR progress events

// Fetch API — NO upload progress await fetch('/upload', { method:'POST', body: formData }) // No progress events — upload is a black box // XHR — has upload progress xhr.upload.addEventListener('progress', e => { const pct = Math.round(e.loaded / e.total * 100); progress.value = pct; });
--:--:--Select any file — watch the XHR progress simulation

Drag to reorder list items

The same Drag and Drop API handles UI element reordering. Drag items up or down. The cursor position relative to each item's midpoint determines insert-before or insert-after.

WCAG 2.5.7 (Dragging Movements, Level AA): a production reorder list needs a keyboard alternative — typically Space to grab, / to move, Enter/Esc to commit/cancel — plus aria-live announcements after each move. This demo shows the drag mechanic only.

--:--:--Drag any task item to reorder — position relative to midpoint determines insert

Custom drag ghost with setDragImage

Drag the items below. Each uses a different custom ghost and dropEffect. The cursor icon changes based on dropEffect (copy, move, link).

📋 Drag me — copy ghost (cursor shows copy icon)
🔀 Drag me — move ghost (cursor shows move icon)

Drop here — see the ghost and cursor change

el.addEventListener('dragstart', (e) => { const ghost = document.createElement('div'); ghost.style.cssText = 'position:fixed;top:-999px;…'; ghost.textContent = 'Custom ghost text'; document.body.appendChild(ghost); e.dataTransfer.setDragImage(ghost, 0, 0); e.dataTransfer.effectAllowed = 'copy'; requestAnimationFrame(() => ghost.remove()); });
--:--:--Drag each item to the drop area — watch the cursor and ghost change
Read the tutorial