File Drop Zone
Drag a file over the drop area and it lifts and glows, its dashed border starts marching and the arrow bobs up. Drop it and the area settles with a little squash and shows what you added. Tap or click it to pick files the usual way.
How to use it
- Paste the snippet just before
</head>. - Wrap a file input in an element with
data-file-drop-zone. - The input is hidden and the zone is built around it, so dropped files post with the form just like picked ones.
Options
Add any of these attributes to the same element.
data-title | The big line. Default Drop files here. |
data-hint | The small line. Default or click to browse. |
data-color | Glow and border colour while a file is over it. Default #6a0dad. |
data-autoplay | A pretend file is dragged in and dropped, over and over while in view. It stops as soon as you use it. |
data-file | The pretend file’s name. Default photo.jpg. |
Phones can’t drag files, so there it’s a big tap target for the file picker. Listen for the files event: e.detail.files is the list of files.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// File Drop Zone: a dashed drop area for files. Drag a file over it and it
// lifts and glows, its border starts marching and the arrow bobs up. Drop
// and it settles with a little squash, showing what you added. Tap or
// click it to pick files the usual way (phones can't drag files).
// Usage: <div data-file-drop-zone><input type="file" name="files" multiple></div>
// The input is hidden and the zone is built around it, so dropped files
// post with the form like picked ones.
// Optional: data-title (big line, default "Drop files here"), data-hint
// (small line, default "or click to browse"), data-color (glow colour,
// default #6a0dad), data-autoplay (a pretend file is dragged in and
// dropped, over and over while in view; it stops as soon as you use it),
// data-file (the pretend file's name, default "photo.jpg").
// Listen for the "files" event: e.detail.files is the FileList.
var DEFAULTS = { title: 'Drop files here', hint: 'or click to browse', color: '#6a0dad', file: 'photo.jpg' };
var UP = '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M14 34h-2a9 9 0 0 1-1.4-17.9A12 12 0 0 1 34 14.5 8 8 0 0 1 36 30.3V30"/><g class="fdz-arrow"><path d="M24 40V23"/><path d="M17.5 29.5L24 23l6.5 6.5"/></g></svg>';
var DONE = '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<circle cx="24" cy="24" r="17"/><path d="M16 24.5l5.5 5.5L32.5 19"/></svg>';
var DOC = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 2h8l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" fill="#fff" stroke="#c4c4cc" stroke-width="1.2"/>' +
'<path d="M14 2v5h5" fill="none" stroke="#c4c4cc" stroke-width="1.2"/><rect x="7" y="11" width="10" height="7" rx="1" fill="#a7d3f7"/><path d="M7.5 17.5l3-3 2 2 1.5-1.5 2.5 2.5" fill="none" stroke="#3b82f6" stroke-width="1.1"/></svg>';
var css =
'.fdz{position:relative;display:grid;place-items:center;box-sizing:border-box;padding:1.6em 1.2em;border-radius:1em;background:#fff;color:#71717a;' +
'text-align:center;cursor:pointer;transition:background-color .3s,box-shadow .3s,color .3s;-webkit-tap-highlight-color:transparent;user-select:none}' +
'.fdz-line{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}' +
'.fdz-line rect{fill:none;stroke:#c4c4cc;stroke-width:2;stroke-dasharray:8 7;transition:stroke .3s}' +
'.fdz-over{background:var(--fdz-soft);color:var(--fdz-color);box-shadow:0 14px 34px -10px var(--fdz-glow),0 0 0 6px var(--fdz-soft)}' +
'.fdz-over .fdz-line rect{stroke:var(--fdz-color);animation:fdz-march .6s linear infinite}' +
'.fdz-added{color:#16a34a}.fdz-added .fdz-line rect{stroke:#16a34a;stroke-dasharray:none}' +
'.fdz:focus-within{outline:2px solid var(--fdz-color);outline-offset:3px}' +
'.fdz-body{display:flex;flex-direction:column;align-items:center;max-width:100%;min-width:0}' +
'.fdz-icon{display:grid;width:2.8em;height:2.8em;margin-bottom:.45em}.fdz-icon svg{grid-area:1/1;width:100%;height:100%;overflow:visible}' +
'.fdz-title{display:block;color:#27272a;font-weight:600;font-size:1.05em;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.fdz-hint{display:block;margin-top:.25em;font-size:.85em}' +
'.fdz-input{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0 0 0 0)}' +
'.fdz-ghost{position:absolute;left:50%;top:50%;z-index:2;display:flex;align-items:center;gap:.4em;padding:.45em .7em .45em .5em;margin:-1.2em 0 0 -3.6em;' +
'border-radius:.6em;background:#fff;box-shadow:0 10px 26px rgba(15,15,30,.18),0 0 0 1px rgba(0,0,0,.06);color:#27272a;font-size:.85em;font-weight:500;' +
'white-space:nowrap;opacity:0;pointer-events:none}' +
'.fdz-ghost svg{width:1.6em;height:1.6em}' +
'@keyframes fdz-march{to{stroke-dashoffset:-15}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function size(n) {
if (n < 1024) return n + ' B';
if (n < 1048576) return Math.round(n / 1024) + ' KB';
return (n / 1048576).toFixed(1) + ' MB';
}
function rgba(hex, a) {
var m = /^#?([0-9a-f]{6})$/i.exec(hex);
if (!m) return 'rgba(106,13,173,' + a + ')';
var n = parseInt(m[1], 16);
return 'rgba(' + (n >> 16) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
}
function setup(el) {
var d = el.dataset;
var mock = !!el.closest('[aria-hidden="true"]');
var input = mock ? null : el.querySelector('input[type="file"]');
var color = d.color || DEFAULTS.color;
var title = d.title || DEFAULTS.title, hint = d.hint || DEFAULTS.hint;
el.classList.add('fdz');
el.style.setProperty('--fdz-color', color);
el.style.setProperty('--fdz-soft', rgba(color, 0.07));
el.style.setProperty('--fdz-glow', rgba(color, 0.45));
var ns = 'http://www.w3.org/2000/svg';
var line = document.createElementNS(ns, 'svg');
line.setAttribute('class', 'fdz-line');
line.setAttribute('aria-hidden', 'true');
var rect = document.createElementNS(ns, 'rect');
line.appendChild(rect);
var body = document.createElement('span');
body.className = 'fdz-body';
body.innerHTML = '<span class="fdz-icon">' + UP + DONE + '</span><span class="fdz-title"></span><span class="fdz-hint"></span>';
var iconBox = body.firstChild, upIcon = iconBox.firstChild, doneIcon = iconBox.lastChild;
var arrow = upIcon.querySelector('.fdz-arrow');
var titleEl = body.children[1], hintEl = body.children[2];
titleEl.textContent = title;
hintEl.textContent = hint;
anime.utils.set(doneIcon, { opacity: 0 });
if (input) {
input.classList.add('fdz-input');
if (!input.getAttribute('aria-label')) input.setAttribute('aria-label', title + ', ' + hint);
}
el.insertBefore(line, el.firstChild);
el.appendChild(body);
var ghost = document.createElement('span');
ghost.className = 'fdz-ghost';
ghost.setAttribute('aria-hidden', 'true');
ghost.innerHTML = DOC + '<span></span>';
ghost.lastChild.textContent = d.file || DEFAULTS.file;
el.appendChild(ghost);
function fit() {
var r = Math.min(16, el.offsetHeight / 2);
rect.setAttribute('x', 1); rect.setAttribute('y', 1);
rect.setAttribute('width', Math.max(0, el.offsetWidth - 2));
rect.setAttribute('height', Math.max(0, el.offsetHeight - 2));
rect.setAttribute('rx', r);
}
fit();
if (window.ResizeObserver) new ResizeObserver(fit).observe(el);
var over = false, bob = null;
function lift(next) {
if (next === over) return;
over = next;
el.classList.toggle('fdz-over', over);
if (next) el.classList.remove('fdz-added');
if (reduced()) return;
anime.utils.remove(el);
anime.animate(el, over ? { y: -6, scale: 1.025, duration: 380, ease: 'out(3)' } : { y: 0, scale: 1, duration: 300, ease: 'out(2)' });
if (bob) { bob.pause(); bob = null; anime.utils.set(arrow, { y: 0 }); }
if (over) {
swap(false);
bob = anime.animate(arrow, { y: [0, -5, 0], duration: 700, loop: true, ease: 'inOut(2)' });
}
}
function swap(done) {
anime.utils.remove([upIcon, doneIcon]);
if (reduced()) { anime.utils.set(upIcon, { opacity: done ? 0 : 1 }); anime.utils.set(doneIcon, { opacity: done ? 1 : 0, scale: 1 }); return; }
anime.animate(upIcon, { opacity: done ? 0 : 1, scale: done ? 0.6 : 1, duration: 220, ease: 'out(2)' });
anime.animate(doneIcon, done ? { opacity: [0, 1], scale: [0.5, 1], rotate: [-30, 0], duration: 520, delay: 120, ease: 'outBack(2.5)' } : { opacity: 0, duration: 160 });
}
function added(names, bytes) {
lift(false);
el.classList.add('fdz-added');
titleEl.textContent = names.length === 1 ? names[0] : names.length + ' files';
hintEl.textContent = 'Added · ' + size(bytes);
swap(true);
if (!reduced()) anime.animate(el, { scaleY: [0.95, 1], scaleX: [1.02, 1], duration: 600, ease: 'outElastic(1, .5)' });
}
function clear() {
el.classList.remove('fdz-added');
titleEl.textContent = title;
hintEl.textContent = hint;
swap(false);
}
function take(files) {
if (!files || !files.length) return;
var names = [], bytes = 0;
for (var i = 0; i < files.length; i++) { names.push(files[i].name); bytes += files[i].size; }
added(names, bytes);
el.dispatchEvent(new CustomEvent('files', { bubbles: true, detail: { files: files } }));
}
if (input) {
var depth = 0;
var hasFiles = function (e) { return e.dataTransfer && Array.prototype.indexOf.call(e.dataTransfer.types || [], 'Files') !== -1; };
el.addEventListener('dragenter', function (e) { if (!hasFiles(e)) return; e.preventDefault(); stop(); depth++; lift(true); });
el.addEventListener('dragover', function (e) { if (!hasFiles(e)) return; e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
el.addEventListener('dragleave', function () { depth = Math.max(0, depth - 1); if (!depth) lift(false); });
el.addEventListener('drop', function (e) {
e.preventDefault();
depth = 0;
var files = e.dataTransfer.files;
if (!input.multiple && files.length > 1) {
var dt = new DataTransfer();
dt.items.add(files[0]);
files = dt.files;
}
try { input.files = files; } catch (err) {}
take(files);
});
el.addEventListener('click', function (e) { stop(); if (e.target !== input) input.click(); });
input.addEventListener('change', function () { if (input.files.length) take(input.files); else clear(); });
input.addEventListener('focus', stop);
}
// Autoplay: a pretend file is dragged in from the corner and dropped.
var timers = [], stopped = false, playing = false;
function later(fn, ms) { timers.push(setTimeout(fn, ms)); }
function stop() {
if (stopped) return;
stopped = true;
timers.forEach(clearTimeout);
anime.utils.remove(ghost);
anime.utils.set(ghost, { opacity: 0 });
if (playing) { lift(false); clear(); }
}
function cycle() {
if (stopped) return;
playing = true;
var w = el.offsetWidth, h = el.offsetHeight;
var sx = Math.min(w * 0.42, 150), sy = -Math.min(h * 0.42, 80);
clear();
anime.animate(ghost, {
opacity: [0, 1], x: [sx, sx * 0.85], y: [sy, sy * 0.8], rotate: [14, 10], scale: [0.9, 1], duration: 350, ease: 'out(2)'
});
later(function () {
anime.animate(ghost, { x: 0, y: -h * 0.06, rotate: -4, duration: 900, ease: 'inOut(3)' });
}, 380);
later(function () { lift(true); }, 900);
later(function () { anime.animate(ghost, { x: [0, 5, -3, 0], rotate: [-4, 2, -4], duration: 800, ease: 'inOut(2)' }); }, 1300);
later(function () {
anime.animate(ghost, { scale: 0.3, opacity: 0, y: h * 0.12, duration: 300, ease: 'in(2)' });
}, 2200);
later(function () { added([d.file || DEFAULTS.file], 2516582); }, 2420);
later(function () { if (!stopped) clear(); playing = false; }, 4800);
later(cycle, 5600);
}
if (on(d.autoplay) && !reduced()) {
new IntersectionObserver(function (entries) {
timers.forEach(clearTimeout);
timers = [];
if (stopped) return;
if (entries[0].isIntersecting) later(cycle, 500);
else { anime.utils.set(ghost, { opacity: 0 }); lift(false); clear(); }
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-file-drop-zone]').forEach(function (el) {
if (el._fdz) return;
el._fdz = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><form action="/upload" method="post" enctype="multipart/form-data">
<div data-file-drop-zone data-hint="PNG or JPG, up to 10 MB">
<input type="file" name="photos" accept="image/*" multiple>
</div>
</form>