File Upload — Web Dropzone Component

Accessible file-upload web component from @keenmate/web-dropzone with drag-and-drop, file previews, validation, upload-progress tracking, and multiple display modes. Framework-agnostic, form-associated, and themed via CSS custom properties — it inherits the active Pure Admin theme through the shared --base-* variables, the same bridge <web-multiselect> uses.

Three orthogonal display axes combine freely:

  • selector-appearancecard · button · minimal · native (what the user clicks / drops onto)
  • list-appearancelist · detailed · grid · badges · popover · none (where picked files appear)
  • card-size — minimal · compact · big (density of the card selector)

Uploads on this page are simulated (a local progress stub) so you can watch the progress UI without a server. Files that fail accept / size / count rules surface as toasts.

Under the Hood — Two Packages

web-dropzone is a workspaces monorepo published as two npm packages — a headless store and a renderer — split so headless consumers never pay for the DOM/CSS layer. The element on this page is the renderer; it bundles the store inside.

@keenmate/web-dropzone-core — the store

No DOM. No CSS. No custom elements. The DropzoneCore class holds the FileState[], runs validation, drives the upload worker pool, and fires the events. It exports the DropzoneStoreAPI interface, the domain types (FileState, FileUploadHandler, …), and pure helpers (formatFileSize, validateFile, …). Install this to build your own React / Vue / Lit / Svelte renderer on the same primitives.

@keenmate/web-dropzone — the renderer

The custom elements (<web-dropzone> + satellites), all CSS, and Floating UI positioning. It depends on the core and bundles it into a single self-contained dist/dropzone.js — the file this demo loads. Install this to drop the elements into a page.

How they compose in <web-dropzone>: the renderer's WebDropzone class extends DropzoneCore, so the convenience single element is its own store — it then drives its internal picker and list through the very same events any external satellite consumes. The renderer only ever reaches the core through two surfaces:

  1. The DropzoneStoreAPI interface — reads (getFiles, getOverallProgress, …) + mutators (addFiles, removeFile, retryFile, …). Every satellite holds store: DropzoneStoreAPI, never the concrete class.
  2. DOM CustomEvents dispatched on the store element with bubbles: true, composed: true so they cross shadow boundaries — file-added, file-progress, file-updated, change, files-rejected, store-ready, …

That is exactly why the satellites in the section above (<web-dropzone-list | progress | indicator>) are peers of the built-ins with no privileged access: each finds its store via for="<id>" and subscribes to those events. You can write your own satellite in any framework the same way.

Which package do I install?

You want to…Install
Drop <web-dropzone> or a built-in satellite into a page@keenmate/web-dropzone
Build a custom React / Vue / Lit / Svelte renderer on the same store@keenmate/web-dropzone-core
The built-in elements and a custom satellite alongside them@keenmate/web-dropzone only — it re-exports the core types & helpers
Installing both explicitly is redundant — the renderer pins the core and bundles its code, so the runtime is self-contained either way (the hard dependency just lets the renderer's .d.ts re-export core types). There is no <web-dropzone-core> element — the core is JS-only: new DropzoneCore(hostEl, config).

Selector Appearance (selector-appearance)

Four visual entry points for picking files.

Click anywhere on the card to browse, or drag-and-drop files.
Use when the picker should sit inline (toolbars, dense forms).
Compact button; the badge appears when files are picked.
Empty: "No file chosen" · 1 file: filename · multiple: "N files chosen". Customize via select-files-text / no-file-chosen-text.

List Appearance (list-appearance)

How the picked files are presented — from a simple vertical list to badge pills or a hidden popover.

Same badge surface as web-multiselect. Add show-thumbnails for image previews.
Popover header shows count + Add more / Clear all; footer pins total size.
Picked: (none)

Card Size (card-size)

Density variants for the card selector. Only meaningful when selector-appearance="card".

Files-Inside Layout (files-inside)

Render the file list inside the dropzone area instead of below it — useful when vertical space is constrained.

Common Combinations

Mixing the three axes covers the patterns users actually want.

Slack / Gmail-style attachment pattern.
Click icon → picker. After files exist → toggles popover.

Satellite Elements (for="<store-id>")

The store and its surfaces can be split into standalone elements that bind to a store by id via for="". Here one <web-dropzone controls="picker"> acts as the store + picker, and the list, overall-progress strip, and floating status indicator are rendered by separate satellites — anywhere on the page.

A <web-dropzone-indicator for="sat-store"> (floating status chip + slide-out drawer) is also mounted for this store — it appears once an upload is active.

Composable Controls (controls + item-controls)

Two allowlists. controls picks which top-level surfaces render; item-controls picks which parts of each file row render. Both are comma-separated; an absent attribute means "render all". Toggle the boxes and add a file to watch it recompose.

Live:
controls — surfaces
item-controls — row parts

Recipes — progress rules in userland

Count- or state-based progress behaviour needs no dedicated attributes — compose it from the primitives above plus the change event and auto-upload.

1–3 files → global strip only. 4+ → both bars. Only the per-row progress token toggles.
With auto-upload="false" files sit pending and the strip stays hidden until you kick off the upload.

Popover Placement (popover-placement)

Positioning uses Floating UI, so it auto-flips on viewport collisions. The attribute sets the preferred initial placement.

Single vs Multiple & Accept Patterns

The multiple attribute defaults to true. Set multiple="false" to lock to a single file. accept uses the same syntax as native <input type="file">.

Submit blocked by native form validity until ≥ 2 files ≥ 1 KB are selected.

Disabled State & Legacy display-mode

The disabled attribute follows HTML convention. The original single-axis display-mode still works as a shorthand (list / detailed / grid / compact=popover); when both are set, the orthogonal attribute wins.

Programmatic API & Events

(none yet — drop or pick a file)
const el = document.getElementById('api-demo')

el.addEventListener('file-added',     e => console.log('added',    e.detail))
el.addEventListener('file-removed',   e => console.log('removed',  e.detail))
el.addEventListener('files-rejected', e => console.log('rejected', e.detail))
el.addEventListener('change',         e => console.log('change',   e.detail))

el.addFiles(fileList)   // programmatic add (FileList or File[])
el.removeFile(id)       // remove by FileState.id
el.clear()              // remove all
const files = el.files  // current FileState[]

// simulated upload driver (this page): (file, onProgress, signal) => Promise
el.uploadFileCallback = simulateUpload({ durationMs: 2500, failRate: 0.15 })

Resources

  • npm Package: @keenmate/web-dropzone (headless store: @keenmate/web-dropzone-core)
  • Repository: github.com/keenmate/web-dropzone
  • Elements: <web-dropzone> + satellites <web-dropzone-picker>, <web-dropzone-list>, <web-dropzone-indicator>, <web-dropzone-progress>
  • CSS Variables: component-local --dz-* + shared --base-* (theme integration, same bridge as web-multiselect / web-grid)
  • Theming: Pure Admin provides --base-* via src/scss/_base-css-variables.scss — the component follows the active theme automatically
Type / for commands, : to search a category, or just start typing

Settings

Press Ctrl+K to preview the palette.
Body text size. All elements scale proportionally.
👤

John Doe

Administrator
  • 📊 Dashboard
  • 📝 Forms
  • 📋 Tables
  • 📊 Data Grid