Flux 2.0

Component

File upload

A dropzone over a native file input, and a list of the chosen files with their progress and errors.

Plain-text source, for agents and clipboard use: /components/source/file-upload.txt

When to use it

For attaching documents to a record: passport scans, supplier contracts, invoices. The native file input covers the dropzone, so a click anywhere opens the picker and a dropped file lands in the input without scripting. Each file joins a list with its size and state. The script checks the accepted types and the size limit, then hands valid files to the page through a flux-upload event; the page uploads them and reports progress, success or failure back to the item.

Not this
Not for importing data that needs mapping and validation row by row; that is a dedicated import flow. Not for a single avatar or logo, where a button that opens the picker is enough.

Examples

Default

Choose or drop files. A file over 10 MB, or of another type, is listed with an error. On this page, every third upload fails so the Retry action can be seen.

Choose files or drop them here

    html
    <div class="flux-field">
      <div class="flux-field__header">
        <label class="flux-field__label" for="passports">Passport scans</label>
        <span class="flux-info" data-flux-info>
          <button class="flux-info__trigger" type="button" aria-label="About passport scans" aria-describedby="passports-info">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.5" />
              <path d="M8 7.25V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
              <circle cx="8" cy="5" r="0.875" fill="currentColor" />
            </svg>
          </button>
          <span class="flux-info__bubble" id="passports-info" role="tooltip" hidden>PDF, JPG or PNG, up to 10 MB each. One file per traveller.</span>
        </span>
      </div>
      <div class="flux-upload" data-flux-upload data-max-mb="10">
        <div class="flux-upload__dropzone">
          <svg class="flux-upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 10.5V2.5M5 5.5l3-3 3 3M2.5 10v3h11v-3" /></svg>
          <p class="flux-upload__prompt"><span class="flux-upload__action">Choose files</span> or drop them here</p>
          <input class="flux-upload__input" id="passports" type="file" multiple accept=".pdf,.jpg,.jpeg,.png" aria-describedby="passports-info" />
        </div>
        <ul class="flux-upload__list" aria-label="Selected files" aria-live="polite"></ul>
      </div>
    </div>

    File states

    Uploaded, uploading with an inline progress bar, and failed with a reason and a retry.

    • passport-anna-jonsdottir.pdf 1.2 MB · Uploaded
    • passport-marianne-hollis.jpg 3.4 MB · Uploading
      45%
    • passport-scan-full-resolution.png 14.8 MB · Larger than 10 MB.
    • passport-olafur-gunnarsson.pdf 0.9 MB · The connection dropped.
    html
    <ul class="flux-upload__list" aria-label="Selected files">
      <li class="flux-upload__item" data-state="done">
        <span class="flux-upload__file-icon"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2.5H5a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V5.5z" /><path d="M9 2.5v3h3" /></svg></span>
        <div class="flux-upload__body">
          <span class="flux-upload__name">passport-anna-jonsdottir.pdf</span>
          <span class="flux-upload__meta"><span class="flux-upload__size">1.2 MB</span> · Uploaded</span>
        </div>
        <div class="flux-upload__actions"><button class="flux-upload__remove" type="button" aria-label="Remove passport-anna-jonsdottir.pdf"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" /></svg></button></div>
      </li>
      <li class="flux-upload__item" data-state="uploading">
        <span class="flux-upload__file-icon"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2.5H5a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V5.5z" /><path d="M9 2.5v3h3" /></svg></span>
        <div class="flux-upload__body">
          <span class="flux-upload__name">passport-marianne-hollis.jpg</span>
          <span class="flux-upload__meta"><span class="flux-upload__size">3.4 MB</span> · Uploading</span>
          <div class="flux-progress flux-progress--sm">
            <progress class="flux-visually-hidden" max="100" value="45" aria-label="Uploading passport-marianne-hollis.jpg">45%</progress>
            <div class="flux-segments" aria-hidden="true"><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item" data-filled></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span><span class="flux-segments__item"></span></div>
          </div>
        </div>
        <div class="flux-upload__actions"><button class="flux-upload__remove" type="button" aria-label="Remove passport-marianne-hollis.jpg"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" /></svg></button></div>
      </li>
      <li class="flux-upload__item" data-state="error">
        <span class="flux-upload__file-icon"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2.5H5a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V5.5z" /><path d="M9 2.5v3h3" /></svg></span>
        <div class="flux-upload__body">
          <span class="flux-upload__name">passport-scan-full-resolution.png</span>
          <span class="flux-upload__meta"><span class="flux-upload__size">14.8 MB</span> · Larger than 10 MB.</span>
        </div>
        <div class="flux-upload__actions"><button class="flux-upload__remove" type="button" aria-label="Remove passport-scan-full-resolution.png"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" /></svg></button></div>
      </li>
      <li class="flux-upload__item" data-state="error">
        <span class="flux-upload__file-icon"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2.5H5a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V5.5z" /><path d="M9 2.5v3h3" /></svg></span>
        <div class="flux-upload__body">
          <span class="flux-upload__name">passport-olafur-gunnarsson.pdf</span>
          <span class="flux-upload__meta"><span class="flux-upload__size">0.9 MB</span> · The connection dropped.</span>
        </div>
        <div class="flux-upload__actions">
          <button class="flux-button flux-button--ghost flux-button--sm" type="button">Retry</button>
          <button class="flux-upload__remove" type="button" aria-label="Remove passport-olafur-gunnarsson.pdf"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" /></svg></button>
        </div>
      </li>
    </ul>

    Anatomy

    PartDescription
    LabelA field label for the input, with an info bubble that states the accepted types and size.
    DropzoneA dashed outline with an icon and a prompt. It turns teal on hover and fills while a file is dragged over it.
    InputThe native file input, invisible, covering the dropzone.
    File listA bordered list of items, hidden while empty, announced politely as it changes.
    ItemA file icon, the name, a mono size and the state, an inline progress bar while uploading, and a remove button.

    API

    NameKindDescription
    flux-uploadclassWraps the dropzone and the list.
    data-flux-uploadattributeWires dragging, checking, listing and the upload events.
    data-max-mbattributeOn flux-upload: the largest file accepted, in megabytes. State it in the info bubble too.
    flux-upload__dropzone | __icon | __prompt | __actionclassThe dropzone and its contents.
    flux-upload__inputclassOn the input type="file". Use accept and multiple as usual. It is cleared after each pick, so the same file can be chosen again.
    flux-upload__list | __itemclassThe file list and each file. data-state is ready, uploading, done or error.
    flux-upload__removeclassThe remove button. Label it "Remove" and the file name.
    flux-uploadelementEvent dispatched per valid file, with detail.file, detail.progress(percent), detail.complete() and detail.fail(message).
    flux-upload-removeelementEvent dispatched when a file is removed, with detail.file.

    States

    StateTreatment
    IdleA dashed border-strong outline on layer-01.
    HoverThe outline turns border-brand.
    Drag overA solid border-brand outline and a surface-brand-subtle fill.
    FocusThe Flux focus ring around the dropzone.
    UploadingAn inline small progress bar below the file name.
    DoneThe file icon turns text-success and the state reads Uploaded.
    ErrorThe icon and the state text turn text-error, with the reason in words. A failed upload offers Retry.
    DisabledA disabled input dims the dropzone and stops it accepting files.

    Keyboard

    KeysAction
    TabFocuses the input, then each item’s actions.
    Enter, SpaceOpens the file picker.

    Accessibility

    • The input is the real control, labelled by the field label, with the constraints in the info bubble it references.
    • The file list is a polite live region, so added files, failures and completions are announced.
    • Every state is in words as well as colour.
    • Removing a file moves focus to the next file’s remove button, or back to the input.

    Tokens

    The tier-3 and tier-2 tokens this component binds to. Change the token, not the component.

    • --flux-upload-padding | -gap | -bg | -bg-active
    • --flux-upload-border | -border-active
    • --flux-upload-icon | -icon-size | -text
    • --flux-upload-item-padding-y | -item-padding-x | -item-gap | -item-divider
    • --flux-upload-meta | -success

    Do and don’t

    Do

    • State the accepted types and size limit in the info bubble before anyone picks a file.
    • Say why a file failed: too large, wrong type, or what the server said.
    • Let people remove a file at any point, including while it uploads.

    Don’t

    • Do not put the size limit below the dropzone as help text.
    • Do not silently drop a file that fails a check.
    • Do not block the rest of the form while files upload.