v1.0.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

SlotDescription
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

PartDescription
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

PropertyDescription
--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.