<bmx-upload>
A file field that takes a drop as readily as a click, and then shows the user what became of every file they gave it: a row each, a bar each, and a way to retry the one that failed without starting the other thirty-nine again.
34 properties · 8 events · 12 methods · 16 parts
Example
Show markup
<div class="row">
<bmx-upload
label="Proof of address"
accept=".pdf,image/*"
max-size="5242880"
description="Held for the form to submit — no request of its own."
></bmx-upload>
</div>
<div class="row">
<bmx-upload
id="ex-upload-many"
label="Supporting documents"
multiple="true"
max-files="6"
accept=".pdf,.docx,image/*"
max-size="10485760"
concurrency="2"
description="Uploaded as they arrive. Cancel one, or retry the one that fails."
></bmx-upload>
</div>
<script type="module">
await customElements.whenDefined('bmx-upload');
// `uploader` is a function, so it is a property. This one pretends to be a
// server: it reports bytes on a timer, honours the abort signal, and fails
// anything with "fail" in its name so the retry button has something to do.
document.getElementById('ex-upload-many').uploader = ({ item, onProgress, signal }) =>
new Promise((resolve, reject) => {
let sent = 0;
const timer = setInterval(() => {
sent = Math.min(item.size, sent + item.size / 12);
onProgress(sent);
if (sent >= item.size) {
clearInterval(timer);
if (item.name.toLowerCase().includes('fail')) {
reject(new Error('The file server rejected this document.'));
} else {
resolve({ url: `/files/${item.name}` });
}
}
}, 180);
// Without this the timer outlives the cancel and the row keeps moving
// after the user has stopped it.
signal.addEventListener('abort', () => {
clearInterval(timer);
reject(new Error('Cancelled.'));
});
});
// A file dropped anywhere *outside* a drop zone makes the browser navigate to
// it, taking the half-filled form with it. The component does not listen on
// the document on your behalf - a component that changes the behaviour of a
// whole page is worse than the problem - so a page with an upload on it is
// worth adding these two lines to.
for (const type of ['dragover', 'drop']) {
addEventListener(type, event => {
// The target is retargeted to the host for a drop inside the component's
// shadow root, so this reaches its own zone as one element. It can also
// be a text node on a drop over bare page text, which is why the type is
// checked before `closest` is called on it.
const over = event.target instanceof Element && event.target.closest('bmx-upload');
if (!over) {
event.preventDefault();
}
});
}
</script>
WHAT IT DOES NOT DO
Talk to your server. It has no opinion about your endpoint, your
authentication or your chunk protocol. Set url and a default
XMLHttpRequest uploader is used; set uploader instead and it calls your
function, with the file, a progress callback and an AbortSignal. Set
neither and the component simply holds the files for the form to submit,
which is the whole of what most forms need.
THE QUEUE IS THE COMPONENT
Everything interesting about an upload control is bookkeeping - per-file
state, retry, cancel, removal, admitting forty files against a limit of
three - and all of it lives in src/core/upload.ts as pure functions with
unit tests. What is left here is a rendering of that queue and the two things
that genuinely need a browser: the drag events and the transport.
FORM PARTICIPATION
The field submits its files as a FormData under its own name, one entry
per file, which is precisely what <input type="file" multiple> does - so a
server handler already written for a native file input needs no change.
It is also invalid while an upload is still running, and invalid if one failed. A form that can be submitted with a half-sent attachment is a form that will be, and the resulting record - a row referring to a file the file store never received - is the kind of defect nobody traces back to the upload control.
DRAG AND DROP
dragenter and dragleave fire for every descendant, so the highlight is
driven by a depth count rather than by the last event seen; the naive version
flickers the moment the pointer crosses a child. Drags carrying anything but
files are ignored, so selected text dragged across the page does not light
the zone up.
Folders are refused by name rather than uploaded. A directory dropped into a
browser arrives as a zero-byte, typeless File, and sending that produces an
empty file on the server with a plausible name on it - a corruption that is
only ever noticed later.
One thing this does not do, deliberately: guard the rest of the page. A
file dropped outside the zone makes the browser navigate to it, losing the
form. Preventing that means listening on window, and a component that
quietly changes the behaviour of the whole document is worse than the problem
- so a page that wants the guard adds four lines of its own, and the documentation shows them.
ACCESSIBILITY
- Dragging is never the only way to do anything: the zone's button opens the platform's own file picker, which meets SC 2.5.7 and is how most people will use it regardless.
- Every row's buttons are named for their file - "Remove report.pdf", not "Remove" repeated eleven times down a list.
- Refusals are announced in a live region present from the first render, and they name the file and the reason.
- The end of a run is summarised once, politely, rather than announcing forty individual completions over the top of whatever the user is reading.
- Removing a row moves focus to the row that took its place, so a keyboard user clearing a list is not returned to the top of the document each time.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
accept |
accept |
string |
— | What the field takes, in the spelling a native file input accepts. |
appearance |
appearance |
BmxUploadAppearance |
'outline' |
Visual treatment. |
autoUpload |
auto-upload |
boolean |
true |
Start uploading as soon as files are added. |
browseText |
browse-text |
string |
— | The picker button's text. |
chunkSize |
chunk-size |
number |
0 |
Bytes per request. Zero sends each file in one. Chunking exists for the file that is too big to survive one request - a proxy's body limit, a timeout, a connection that drops at four minutes. The protocol the built-in uploader speaks is a convention, not a standard; see upload-transport.ts, and replace uploader if yours differs. |
concurrency |
concurrency |
number |
3 |
How many files may be in flight at once. |
description |
description |
string |
— | Help text below the field. |
disabled |
disabled |
boolean |
false |
Disable the field. |
errorText |
error-text |
string |
— | An error supplied by the consumer - a server response, typically. |
fieldName |
field-name |
string |
'file' |
The form field each file is sent under by the built-in uploader. |
fullWidth |
full-width |
boolean |
true |
Stretch to the width of the container. On by default: a list wants room. |
headers |
property only | Record<string, string> |
— | Extra request headers for the built-in uploader. Accepts the JSON spelling of the object as well, for templates that can only write attributes. See src/core/markup.ts. |
hideLabel |
hide-label |
boolean |
false |
Hide the label visually while keeping it for assistive technology. |
hint |
hint |
string |
— | Replace the generated line describing what the field accepts. The default is composed from accept, maxSize and maxFiles, so a field that changes its own limits cannot end up describing the old ones. |
label |
label |
string |
— | The field's label. Required unless the label slot is used. |
maxFiles |
max-files |
number |
— | How many files may be queued at once. |
maxSize |
max-size |
number |
— | Largest file allowed, in bytes. |
messages |
property only | BmxFieldMessages |
— | Replacements for the default validity wording, by reason. Accepts the JSON spelling of the object as well, for templates that can only write attributes. See src/core/markup.ts. |
method |
method |
string |
'POST' |
HTTP method for the built-in uploader. |
minSize |
min-size |
number |
— | Smallest file allowed, in bytes. Catches the empty placeholder file. |
multiple |
multiple |
boolean |
false |
Accept more than one file. Off by default, as on <input type="file">. A single-file field replaces its file when a new one is chosen rather than refusing it. |
name |
name |
string |
— | The field's name in the form it belongs to. |
promptText |
prompt-text |
string |
'Drag and drop files here' |
The line inside the drop zone. |
readonly |
readonly |
boolean |
false |
Show the files without letting the user add or remove any. |
refusalMessages |
refusal-messages |
Partial<Record<BmxUploadRejection, string>> | string |
— | Replacements for the wording of a refusal. {name} becomes the file name. Accepts the JSON spelling as well. The | string in the type is what makes refusal-messages exist as an attribute - see the note on bmx-date-picker's disabledDaysOfWeek, and src/core/markup.ts. |
required |
required |
boolean |
false |
Require at least one file. |
shape |
shape |
BmxShape |
'rounded' |
Corner treatment. |
size |
size |
BmxSize |
'md' |
Size step. |
submitValue |
submit-value |
BmxUploadSubmitValue |
'auto' |
What the field contributes to its form. |
tone |
tone |
BmxTone |
'primary' |
Semantic colour role, used for the focus ring and the bars. |
uploader |
property only | BmxUploader |
— | Your own transport, in place of the built-in one. |
url |
url |
string |
— | Endpoint for the built-in uploader. Leave unset to hold files for the form. |
validateOn |
validate-on |
BmxValidateOn |
'blur' |
When the field is willing to reveal a problem. |
withCredentials |
with-credentials |
boolean |
false |
Send credentials on a cross-origin upload. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
BmxUploadChangeDetail |
Fired whenever the set of files changes. |
bmxRefuse |
BmxUploadRefusedDetail |
Fired when files are turned away, with the reason for each. |
bmxUploadEnd |
BmxUploadEndDetail |
Fired once when nothing is left to upload. |
bmxUploadError |
BmxUploadFileDetail |
Fired when a file could not be uploaded. |
bmxUploadProgress |
BmxUploadProgressDetail |
Fired as bytes move. |
bmxUploadStart |
BmxUploadFileDetail |
Fired as each file is handed to the uploader. |
bmxUploadSuccess |
BmxUploadFileDetail |
Fired when a file has been uploaded. |
bmxValidityChange |
BmxUploadValidityDetail |
Fired whenever the resolved validity changes. |
Methods
| Method | Signature | Description |
|---|---|---|
addFiles |
addFiles(files: File[] | FileList) => Promise<void> |
Add files without going through the picker or a drop. |
cancelFile |
cancelFile(id: string) => Promise<void> |
Stop a file that is uploading, or take a queued one out of the running. |
checkValidity |
checkValidity() => Promise<boolean> |
Validate now and return whether the field passed, without revealing it. |
clear |
clear() => Promise<void> |
Empty the queue, stopping anything in flight. |
getFiles |
getFiles() => Promise<File[]> |
The files the field is holding, in queue order. |
getItems |
getItems() => Promise<BmxUploadItem<File>[]> |
The queue itself - each file with its state, progress and any error. |
openPicker |
openPicker() => Promise<void> |
Open the platform's file picker, as the button does. |
removeFile |
removeFile(id: string) => Promise<void> |
Remove a file from the queue entirely. |
reportValidity |
reportValidity() => Promise<boolean> |
Validate, reveal any problem, and focus the field if it has one. |
retryFile |
retryFile(id: string) => Promise<void> |
Put a failed or cancelled file back in the queue and start it again. |
setFocus |
setFocus(options?: FocusOptions) => Promise<void> |
Focus the field's picker button. |
start |
start() => Promise<void> |
Begin uploading. Only needed when autoUpload is off. |
Slots
| Slot | Description |
|---|---|
description |
Rich help text, in place of the description property. |
label |
Rich label content, in place of the label property. |
prompt |
Replaces the wording inside the drop zone. |
CSS shadow parts
| Part | Description |
|---|---|
browse |
The button that opens the file picker. |
cancel |
A row's cancel button. |
description |
The help text. |
error |
The error message. |
field |
The drop zone. |
file |
One file's row. |
file-meta |
A file's size and state. |
file-name |
A file's name. |
hint |
The line describing what the field accepts. |
label |
The label element. |
list |
The list of files. |
notices |
The live region holding refusal messages. |
overall |
The progress bar across the whole queue. |
progress |
A file's progress bar. |
remove |
A row's remove button. |
retry |
A row's retry button. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-upload-background |
The zone's background. |
--bmx-upload-bar-height |
Thickness of the progress bars. |
--bmx-upload-bar-track |
The unfilled part of a progress bar. |
--bmx-upload-border-color |
The zone's border colour in its resting state. |
--bmx-upload-border-width |
Border width of the drop zone. |
--bmx-upload-drop-background |
The zone's background while a drag is over it. |
--bmx-upload-font-size |
Base font size for the zone and the file list. |
--bmx-upload-label-font-size |
The label's font size. |
--bmx-upload-list-max-block-size |
Height at which the file list starts scrolling. |
--bmx-upload-padding-block |
Vertical padding inside the drop zone. |
--bmx-upload-padding-inline |
Horizontal padding inside the drop zone. |
--bmx-upload-radius |
Corner radius of the zone and the file rows. |
--bmx-upload-row-background |
Background of a file row. |
--bmx-upload-stack-gap |
Space between the label, the zone, the list and the supporting text. |
--bmx-upload-support-font-size |
Font size of the description, error, hint and file meta. |