material-dropzone

Drag-and-drop multi-file upload area. A native <input type="file"> carries the payload, so a plain Django multipart form gets request.FILES.getlist(name) — dropped files included. Per-file progress driven by the consumer.

Basic — form posting, image previews

Drop files or click to browse; images get thumbnails. Files accumulate across drops and post with the form (check FormData below — file names appear as entries).

Submit Reset
(submit to see FormData)

Validation — accept, max-size, max-files

Images only, ≤ 512 KB each, at most 3 files. Rejected files fire materialFileReject with a reason. The cancelable materialFileAdd hook here also rejects names containing "draft".

(no rejections yet)

Simulated upload — per-file progress

The demo "uploads" each added file (~2s, third of files fail) and drives the rows via setProgress(file, pct | 'done' | 'error'). Real code does the same around fetch/XHR.

RTL (hard-coded)

Sample usage (Django)
<!-- zero-JS: files post with the form -->
<form method="post" enctype="multipart/form-data">
  <material-dropzone name="attachments" accept="image/*,.pdf"
                     max-size="10485760" max-files="8" required></material-dropzone>
  <material-button type="submit" variant="filled">Save</material-button>
</form>

# views.py
for f in request.FILES.getlist('attachments'): ...

// JS-driven upload with progress (upload stays your code):
zone.addEventListener('fileChange', (e) => {
  for (const file of e.detail.added) {
    uploadWithProgress('/api/upload/', file, pct => zone.setProgress(file, pct))
      .then(() => zone.setProgress(file, 'done'))
      .catch(err => zone.setProgress(file, 'error', err.message));
  }
});