UI Components Medium
File Upload Dropzone
Drag-and-drop file upload zone with file list, type/size validation, progress simulation, and remove button.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 480px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.section {
margin-bottom: 1.5rem;
}
/* ── Dropzone ── */
.dropzone {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 2.5rem 1.5rem;
border: 2px dashed rgba(255, 255, 255, 0.12);
border-radius: 14px;
background: rgba(255, 255, 255, 0.025);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
text-align: center;
}
.dropzone:hover,
.dropzone:focus-visible {
border-color: rgba(99, 179, 237, 0.45);
background: rgba(99, 179, 237, 0.04);
outline: none;
}
.dropzone.is-dragging {
border-color: #38bdf8;
background: rgba(56, 189, 248, 0.08);
}
.dropzone.is-error {
border-color: rgba(239, 68, 68, 0.5);
background: rgba(239, 68, 68, 0.04);
}
.file-input {
position: absolute;
inset: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.dropzone-icon {
width: 3rem;
height: 3rem;
color: #334155;
margin-bottom: 0.25rem;
}
.dropzone-icon svg {
width: 100%;
height: 100%;
}
.is-dragging .dropzone-icon {
color: #38bdf8;
}
.dropzone-primary {
font-size: 0.9375rem;
font-weight: 600;
color: #cbd5e1;
}
.dropzone-secondary {
font-size: 0.875rem;
color: #475569;
}
.dropzone-link {
color: #38bdf8;
text-decoration: underline;
}
.dropzone-hint {
font-size: 0.75rem;
color: #334155;
margin-top: 0.25rem;
}
.dropzone-error-msg {
font-size: 0.8125rem;
color: #f87171;
margin-top: 0.25rem;
}
/* ── File list ── */
.file-list {
list-style: none;
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.file-item {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
gap: 0.25rem 0.75rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 10px;
align-items: center;
}
.file-name {
font-size: 0.875rem;
font-weight: 500;
color: #cbd5e1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size {
font-size: 0.75rem;
color: #475569;
grid-column: 1;
}
.file-remove {
grid-column: 2;
grid-row: 1 / 3;
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
background: rgba(255, 255, 255, 0.06);
border: none;
border-radius: 6px;
color: #475569;
cursor: pointer;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
align-self: center;
}
.file-remove:hover {
background: rgba(239, 68, 68, 0.2);
color: #f87171;
}
.file-remove svg {
width: 0.875rem;
height: 0.875rem;
}
/* ── Progress bar ── */
.file-progress-wrap {
grid-column: 1;
height: 3px;
background: rgba(255, 255, 255, 0.08);
border-radius: 999px;
overflow: hidden;
margin-top: 0.25rem;
}
.file-progress {
height: 100%;
border-radius: 999px;
background: #38bdf8;
transition: width 0.25s ease;
width: 0%;
}
.file-item.is-done .file-progress {
background: #22c55e;
}
.file-item.is-error .file-progress {
background: #ef4444;
}
.file-item.is-error .file-name {
color: #f87171;
}var dropzone = document.getElementById("dropzone");
var fileInput = document.getElementById("file-input");
var fileList = document.getElementById("file-list");
var MAX_SIZE = 5 * 1024 * 1024; // 5 MB
var ACCEPT_RE = /^image\//;
var dragCounter = 0;
function formatBytes(n) {
if (n < 1024) return n + " B";
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
return (n / (1024 * 1024)).toFixed(1) + " MB";
}
function simulateProgress(item, bar) {
var pct = 0;
var iv = setInterval(function () {
pct += Math.random() * 18 + 4;
if (pct >= 100) {
pct = 100;
clearInterval(iv);
item.classList.add("is-done");
}
bar.style.width = pct + "%";
}, 120);
}
function addFile(file) {
var isInvalidType = !ACCEPT_RE.test(file.type);
var isTooBig = file.size > MAX_SIZE;
var li = document.createElement("li");
li.className = "file-item" + (isInvalidType || isTooBig ? " is-error" : "");
var name = document.createElement("span");
name.className = "file-name";
name.textContent = file.name;
var size = document.createElement("span");
size.className = "file-size";
size.textContent = isInvalidType
? "Invalid file type"
: isTooBig
? "File too large (" + formatBytes(file.size) + ")"
: formatBytes(file.size);
var progressWrap = document.createElement("div");
progressWrap.className = "file-progress-wrap";
var progressBar = document.createElement("div");
progressBar.className = "file-progress";
if (isInvalidType || isTooBig) progressBar.style.width = "100%";
progressWrap.appendChild(progressBar);
var removeBtn = document.createElement("button");
removeBtn.className = "file-remove";
removeBtn.setAttribute("aria-label", "Remove " + file.name);
removeBtn.innerHTML =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
removeBtn.addEventListener("click", function () {
li.remove();
});
li.appendChild(name);
li.appendChild(size);
li.appendChild(progressWrap);
li.appendChild(removeBtn);
fileList.appendChild(li);
if (!isInvalidType && !isTooBig) {
simulateProgress(li, progressBar);
}
}
function handleFiles(files) {
Array.from(files).forEach(addFile);
// reset so same file can be re-added
fileInput.value = "";
}
// Click to open file picker
dropzone.addEventListener("click", function (e) {
if (e.target !== fileInput) fileInput.click();
});
dropzone.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
fileInput.click();
}
});
fileInput.addEventListener("change", function () {
handleFiles(fileInput.files);
});
// Drag and drop
dropzone.addEventListener("dragenter", function (e) {
e.preventDefault();
dragCounter++;
dropzone.classList.add("is-dragging");
});
dropzone.addEventListener("dragover", function (e) {
e.preventDefault();
});
dropzone.addEventListener("dragleave", function () {
dragCounter--;
if (dragCounter <= 0) {
dragCounter = 0;
dropzone.classList.remove("is-dragging");
}
});
dropzone.addEventListener("drop", function (e) {
e.preventDefault();
dragCounter = 0;
dropzone.classList.remove("is-dragging");
if (e.dataTransfer.files.length) handleFiles(e.dataTransfer.files);
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File Upload Dropzone</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">File Upload Dropzone</h1>
<p class="demo-sub">Drag files here or click to browse. Images only, max 5 MB.</p>
<section class="section">
<div
class="dropzone"
id="dropzone"
role="button"
tabindex="0"
aria-label="File upload area. Click or drag files here."
>
<input
type="file"
id="file-input"
class="file-input"
accept="image/*"
multiple
aria-hidden="true"
tabindex="-1"
/>
<div class="dropzone-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 16 12 12 8 16"/>
<line x1="12" y1="12" x2="12" y2="21"/>
<path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/>
</svg>
</div>
<p class="dropzone-primary">Drop images here</p>
<p class="dropzone-secondary">or <span class="dropzone-link">click to browse</span></p>
<p class="dropzone-hint">PNG, JPG, GIF, WebP — max 5 MB each</p>
</div>
<ul class="file-list" id="file-list" aria-label="Uploaded files" aria-live="polite"></ul>
</section>
</div>
<script src="script.js"></script>
</body>
</html>File Upload Dropzone
Drag-and-drop upload area backed by a hidden <input type="file">. Accepted files are listed below the zone with a simulated progress bar. Invalid type or oversized files show an error state.
Features
- Drag-and-drop with visual dragging state
- Click to open the native file picker
- Type validation (images by default)
- Size validation (5 MB limit)
- Animated progress simulation
- Remove file button
Implementation
dragenter / dragover / dragleave / drop events manage the dragging state. FileReader is not used — files are read via File.name / File.size metadata only. Progress is simulated with setInterval for demo purposes.