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.
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
<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
-
passport-marianne-hollis.jpg
-
passport-scan-full-resolution.png
-
passport-olafur-gunnarsson.pdf
<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
| Part | Description |
|---|---|
| Label | A field label for the input, with an info bubble that states the accepted types and size. |
| Dropzone | A dashed outline with an icon and a prompt. It turns teal on hover and fills while a file is dragged over it. |
| Input | The native file input, invisible, covering the dropzone. |
| File list | A bordered list of items, hidden while empty, announced politely as it changes. |
| Item | A file icon, the name, a mono size and the state, an inline progress bar while uploading, and a remove button. |
API
| Name | Kind | Description |
|---|---|---|
flux-upload | class | Wraps the dropzone and the list. |
data-flux-upload | attribute | Wires dragging, checking, listing and the upload events. |
data-max-mb | attribute | On flux-upload: the largest file accepted, in megabytes. State it in the info bubble too. |
flux-upload__dropzone | __icon | __prompt | __action | class | The dropzone and its contents. |
flux-upload__input | class | On 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 | __item | class | The file list and each file. data-state is ready, uploading, done or error. |
flux-upload__remove | class | The remove button. Label it "Remove" and the file name. |
flux-upload | element | Event dispatched per valid file, with detail.file, detail.progress(percent), detail.complete() and detail.fail(message). |
flux-upload-remove | element | Event dispatched when a file is removed, with detail.file. |
States
| State | Treatment |
|---|---|
| Idle | A dashed border-strong outline on layer-01. |
| Hover | The outline turns border-brand. |
| Drag over | A solid border-brand outline and a surface-brand-subtle fill. |
| Focus | The Flux focus ring around the dropzone. |
| Uploading | An inline small progress bar below the file name. |
| Done | The file icon turns text-success and the state reads Uploaded. |
| Error | The icon and the state text turn text-error, with the reason in words. A failed upload offers Retry. |
| Disabled | A disabled input dims the dropzone and stops it accepting files. |
Keyboard
| Keys | Action |
|---|---|
Tab | Focuses the input, then each item’s actions. |
Enter, Space | Opens 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.