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-appearance— card · button · minimal · native (what the user clicks / drops onto)list-appearance— list · 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:
- The
DropzoneStoreAPIinterface — reads (getFiles,getOverallProgress, …) + mutators (addFiles,removeFile,retryFile, …). Every satellite holdsstore: DropzoneStoreAPI, never the concrete class. - DOM
CustomEvents dispatched on the store element withbubbles: true, composed: trueso 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 |
.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.
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.
web-multiselect. Add show-thumbnails for image previews.
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.
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.
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.
progress token toggles.
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">.
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-*viasrc/scss/_base-css-variables.scss— the component follows the active theme automatically