Drag and Drop
A file picker works. But dragging a file straight onto the page? That feels effortless — and Resumable.js makes it trivial with assignDrop(). This example builds a complete drag-and-drop upload zone with visual hover feedback, a fallback browse button, and progress indicators. For more patterns, browse the Examples hub.
How assignDrop and assignBrowse Work
assignDrop(domNode) registers dragover, dragenter, dragleave, and drop listeners on the element you pass in. When a user drops files, they're added to the upload queue automatically. assignBrowse(domNode) creates a hidden file input linked to the given element — clicking the element opens the native file picker.
You can call both on the same wrapper, or split them across separate elements. Either way, added files fire the same fileAdded event.
Complete Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Drag & Drop Upload</title>
<style>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 2rem auto; }
.dropzone {
border: 2px dashed #94a3b8;
border-radius: 8px;
padding: 3rem 1.5rem;
text-align: center;
color: #64748b;
transition: border-color 0.15s, background 0.15s;
cursor: pointer;
}
/* Active drag-over state */
.dropzone.hover {
border-color: #2563eb;
background: #eff6ff;
color: #1e40af;
}
.dropzone p { margin: 0 0 0.5rem; font-size: 1.125rem; }
.dropzone .hint { font-size: 0.8rem; }
#file-list { list-style: none; padding: 0; margin-top: 1.5rem; }
#file-list li { display: flex; justify-content: space-between; padding: 0.4rem 0; border-bottom: 1px solid #e5e7eb; }
.bar { height: 4px; background: #e5e7eb; border-radius: 2px; margin-top: 0.25rem; }
.bar-fill { height: 100%; background: #2563eb; border-radius: 2px; transition: width 0.2s; }
</style>
</head>
<body>
<h1>Upload Files</h1>
<div id="drop-area" class="dropzone">
<p>Drag files here</p>
<span class="hint">or click to browse</span>
</div>
<ul id="file-list"></ul>
<script src="resumable.js"></script>
<script>
var dropArea = document.getElementById('drop-area');
var fileList = document.getElementById('file-list');
var r = new Resumable({
target: '/api/upload',
chunkSize: 1 * 1024 * 1024,
simultaneousUploads: 3,
testChunks: false
});
if (!r.support) {
dropArea.innerHTML = '<p>Browser not supported.</p>';
} else {
// Bind both drop and browse to the same element
r.assignDrop(dropArea);
r.assignBrowse(dropArea);
// --- Visual hover feedback ---
dropArea.addEventListener('dragenter', function () {
dropArea.classList.add('hover');
});
dropArea.addEventListener('dragleave', function (e) {
// Only remove if we've left the dropzone entirely
if (!dropArea.contains(e.relatedTarget)) {
dropArea.classList.remove('hover');
}
});
dropArea.addEventListener('drop', function () {
dropArea.classList.remove('hover');
});
// --- File events ---
r.on('fileAdded', function (file) {
var li = document.createElement('li');
li.id = 'file-' + file.uniqueIdentifier;
li.innerHTML =
'<div><span class="name">' + file.fileName + '</span>' +
'<div class="bar"><div class="bar-fill" style="width:0%"></div></div></div>' +
'<span class="pct">0%</span>';
fileList.appendChild(li);
r.upload();
});
r.on('fileProgress', function (file) {
var pct = Math.round(file.progress() * 100);
var li = document.getElementById('file-' + file.uniqueIdentifier);
if (li) {
li.querySelector('.bar-fill').style.width = pct + '%';
li.querySelector('.pct').textContent = pct + '%';
}
});
r.on('fileSuccess', function (file) {
var li = document.getElementById('file-' + file.uniqueIdentifier);
if (li) {
li.querySelector('.pct').textContent = '✓';
li.querySelector('.bar-fill').style.width = '100%';
li.querySelector('.bar-fill').style.background = '#16a34a';
}
});
r.on('fileError', function (file, msg) {
var li = document.getElementById('file-' + file.uniqueIdentifier);
if (li) {
li.querySelector('.pct').textContent = '✗';
li.querySelector('.bar-fill').style.background = '#dc2626';
}
});
}
</script>
</body>
</html>
The dragleave Gotcha
Why check e.relatedTarget? Because dragleave fires when the cursor moves over a child element inside the dropzone — not just when it actually leaves. Without that guard, the hover class flickers on and off as the user drags over nested nodes. It's a subtle bug that has haunted file-upload UIs for years.
An alternative approach: use a counter. Increment on dragenter, decrement on dragleave, and only remove the class when the counter hits zero. Both techniques work; the relatedTarget check is just shorter.
Separating Drop and Browse Targets
Sometimes you want the dropzone and the browse button to be visually distinct — a large drop area with a small button underneath:
r.assignDrop(document.getElementById('drop-area'));
r.assignBrowse(document.getElementById('browse-btn'));
Each call is independent. You can even assign multiple drop targets:
r.assignDrop(document.getElementById('zone-a'));
r.assignDrop(document.getElementById('zone-b'));
Files from any source flow into the same Resumable instance and fire the same events. Handy when your UI has multiple entry points — a sidebar panel and a main content area, for example.
Restricting File Types
Pair the drop zone with fileType to prevent unexpected formats:
var r = new Resumable({
target: '/api/upload',
fileType: ['png', 'jpg', 'jpeg', 'gif'],
fileTypeErrorCallback: function (file) {
alert(file.fileName + ' is not an accepted image format.');
}
});
Resumable.js checks the extension, not the MIME type. Server-side validation is still essential — client-side checks are a UX convenience, never a security boundary.
Accessibility Considerations
A drag-and-drop zone is invisible to keyboard-only users. Always pair it with an assignBrowse fallback so the file picker remains reachable via Tab and Enter. Add an aria-label on the dropzone element and visually announce upload status changes with an aria-live region for screen readers.
Good upload UX works for everyone, not just people with a mouse.
