File Upload — <mv-file-upload>
File drop zone: drag and drop with highlighting (and visual rejection of disallowed types), a list with image thumbnails, icons, sizes and removal, type/size/count validation, and per-file progress; form-associated (FormData).
Category Forms Type Web Component (<mv-file-upload>) Status stable Keywords file-upload, dropzone, drag-and-drop, upload, attachments, progress, form-associated
When to use
Users attach documents or images by drag and drop with type, size and count limits enforced Uploads should show per-file progress, thumbnails and a way to remove a file before submitting A form must submit selected files natively through FormData
Avoid when
A single optional file is picked in a dense form where a drop zone takes too much room → use Input instead Files are dropped into a chat message composer → use AI Chat instead
Get File Upload. Included in every plan: the Pro pack ships all 226 components with the CLI and the MCP server used below.
See plans
Install
node scripts/add.mjs file-upload --out ./src/marvelous
AI agent with the Marvelous UI MCP server: install_components({ slugs: ["file-upload"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/file-upload/file-upload.js, components/file-upload/file-upload.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<form id="file-upload-demo-form" style="display:grid;gap:.5rem;width:min(100%,30rem)">
<label for="file-upload-demo" style="font-size:.875rem;font-weight:500">Supporting documents</label>
<mv-file-upload id="file-upload-demo" name="attachments" accept="image/*,.pdf,.docx,.xlsx" multiple max-size="5 MB" max-files="6"></mv-file-upload>
</form>
<script type="module">
const up = document.getElementById("file-upload-demo");
// Simulated upload: progress ticks until 100%, one in eight fails.
let seeding = false;
const simulate = (file, from = 0) => {
let pct = from;
const fail = Math.random() < 0.125;
const tick = () => {
if (!up.files.includes(file)) return;
pct += 6 + Math.random() * 16;
if (fail && pct > 55) return up.setError(file, "Connection lost");
up.setProgress(file, Math.min(100, pct));
if (pct < 100) setTimeout(tick, 180 + Math.random() * 260);
};
setTimeout(tick, 150);
};
up.addEventListener("mv-add", (e) => { if (!seeding) e.detail.files.forEach((f) => simulate(f)); });
// Seed the demo with a few realistic files.
customElements.whenDefined("mv-file-upload").then(async () => {
const canvas = document.createElement("canvas");
canvas.width = 160; canvas.height = 160;
const ctx = canvas.getContext("2d");
const g = ctx.createLinearGradient(0, 0, 160, 160);
g.addColorStop(0, "#f6d365"); g.addColorStop(0.55, "#fda085"); g.addColorStop(1, "#8e7cc3");
ctx.fillStyle = g; ctx.fillRect(0, 0, 160, 160);
ctx.fillStyle = "rgba(255,255,255,.85)";
ctx.beginPath(); ctx.arc(112, 48, 18, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "rgba(40,30,70,.55)";
ctx.beginPath(); ctx.moveTo(0, 160); ctx.lineTo(56, 84); ctx.lineTo(96, 128); ctx.lineTo(124, 100); ctx.lineTo(160, 140); ctx.lineTo(160, 160); ctx.fill();
const blob = await new Promise((r) => canvas.toBlob(r, "image/jpeg", 0.8));
const pad = (n) => new Uint8Array(n);
const photo = new File([blob, pad(1_380_000)], "site-facade.jpg", { type: "image/jpeg" });
const quote = new File([pad(842_000)], "signed-quote-2026-09.pdf", { type: "application/pdf" });
const plan = new File([pad(2_310_000)], "floor-plan-level-2.docx", { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
seeding = true;
const [a, b, c] = up.addFiles([photo, quote, plan]);
seeding = false;
up.setProgress(a, 100);
up.setProgress(b, 100);
up.setProgress(c, 64);
setTimeout(() => simulate(c, 64), 2500);
});
</script>
API
Attributes
Name Type Default Description namestring Name of the form entries (one per file). acceptstring Accepted types, like <input type=file>: “image/*,.pdf,application/zip”. multipleboolean Multiple files (otherwise a new file replaces the previous one). max-sizestring Max size per file: “5 MB”, “500KB”, “1048576”. max-filesnumber Max number of files (with multiple). labelstring Main text of the zone (default: “Drag and drop files or browse”). hintstring Hint text (generated default: types · size · count). layoutlist | grid listDetailed list or thumbnail grid. requiredboolean Native validation (valueMissing). disabledboolean Disables the zone. localestring en-USSize formatting (Intl: “2.4 MB”).
Properties
Name Type Description filesFile[] Kept files (read-only). itemsArray<{ id, file, status, progress, error }> State of each file: idle | uploading | done | error.
Methods
Name Description open()Opens the native file picker. addFiles(files)Validates and adds files; returns the accepted ones. remove(file | id)Removes a file (and releases its preview). clear()Clears the list. setProgress(file | id, pct)Updates the progress bar (100 = uploaded). setError(file | id, message)Marks a file as failed. checkValidity() / reportValidity()Native validation.
Events
Name Description mv-adddetail: { files } — accepted files (hook point to start the upload). mv-removedetail: { file }. mv-changedetail: { files } — full list after an add/remove. mv-rejectdetail: { errors: [{ file, reason: type | size | count, message }] }.
Content structure
Name Description child contentReplaces the default content of the drop zone.
CSS classes
Name Description mv-file-upload-zone / -item / -thumb / -progress / -errorsGenerated parts; data-dragging / data-drag-reject on the host while dragging.
CSS variables
Name Default Description --mv-file-upload-width32remMax width. --mv-file-upload-zone-height10remMin height of the zone. --mv-file-upload-radiusvar(--mv-radius-xl)Zone radius.
Accessibility
The zone is a focusable role=button (Enter/Space open the picker), named by the <label for> and described by the hint text. Validation errors go in a role=alert region; additions, removals and completed uploads are announced through a polite live region. Each file has a “Remove <name>” button (focus moves to the next one) and a role=progressbar bar while uploading.