The drag-drop uploader with preview โ a portfolio-grade component in ~40 lines.
The drop zone
const zone = document.querySelector(".drop-zone");
["dragover", "dragleave", "drop"].forEach((evt) =>
zone.addEventListener(evt, (e) => e.preventDefault()) // required! else browser opens the file
);
zone.addEventListener("dragover", () => zone.classList.add("hot"));
zone.addEventListener("dragleave", () => zone.classList.remove("hot"));
zone.addEventListener("drop", (e) => {
zone.classList.remove("hot");
handleFiles(e.dataTransfer.files);
});
// clicking the zone opens the picker too
zone.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", () => handleFiles(fileInput.files));Validate + preview
function handleFiles(files) {
for (const file of files) {
if (!file.type.startsWith("image/")) return alert("Images only");
if (file.size > 2 * 1024 * 1024) return alert("Max 2MB");
const img = document.createElement("img");
img.src = URL.createObjectURL(file); // instant preview, no FileReader needed
img.onload = () => URL.revokeObjectURL(img.src); // free the memory
previews.append(img);
}
}Upload
const fd = new FormData();
fd.append("photo", file);
await fetch("/api/upload", { method: "POST", body: fd }); // NO Content-Type header โ browser sets itValidate size/type on the server again โ client checks are convenience only.