Multi File Upload
Multi File Upload widgets are Input Fields that upload several files through a service you provide. They are the array variant of the File Upload: same box, same upload button, same progress bar and same uploadService. Files upload one at a time, every finished file becomes a pill inside the box, and the value bound at the widget’s path is an array of file envelopes, { id, name, size, type, status, error?, data? }, never the browser File objects.
A failed file pauses the queue until it is retried or removed, so no file behind it uploads while the user has not decided what to do with it. There is no prop that caps the number of files; the widget never silently blocks a pick, never uploads without a service, and never retries on its own.
The pills are the same pills used by Tags. ArrowLeft and ArrowRight move between them, Delete removes the focused pill, and when the box is too narrow to show every file they collapse into a count bubble that opens a list of them.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('attachments', { label: 'Attachments', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "attachments", "label": "Attachments" } ]}Upload service
Section titled “Upload service”The widget does not ship a transport. When the user picks or drops files, the widget hands each File, one at a time and in order, to the uploadService you inject through the form’s dependencies, next to the markdown parser. It calls upload for the first file as soon as the files are picked, drives the progress bar from your onProgress calls, stores whatever upload resolved with in that file’s data, and awaits remove before dropping a pill so your server can delete the file.
Keep the service object at module level or memoized. A new dependencies identity is a new form config and re-initializes the form.
The contract
Section titled “The contract”Both types are exported by @golemui/gui-shared.
type UploadService = { upload( file: File, ctx: { id: string; // FileItem.id, stable across progress and retry path: string; // the widget path, useful to route uploads by field onProgress?: (percentage: number) => void; // 0 to 100, drives the bar signal: AbortSignal; // aborted when the user cancels the upload }, ): Promise<unknown>; // the resolved value becomes FileItem.data remove?(item: FileItem): Promise<void>; // awaited behind a spinner before the pill is removed};
type FileItem = { id: string; name: string; size: number; // bytes type: string; // MIME type as reported by the browser status: 'uploading' | 'uploaded' | 'error'; error?: string; // the reason shown below the input when status is "error" data?: unknown; // the server response, verbatim};A rejected upload marks the file as error, shows the rejection message below the input (or Upload failed when the error carries no message), offers a retry button and pauses the queue until that file is retried or removed. A rejected remove keeps the pill in place.
Wiring the service
Section titled “Wiring the service”In the Programmatic API, the service goes inside formConfig.dependencies.
import { gui } from '@golemui/gui-shared';import { GuiForm } from '@golemui/gui-react';import { uploadService } from './upload-service';
const config = { formDef: [gui.inputs.multiFileUpload('attachments', { label: 'Attachments' })], formConfig: { dependencies: { uploadService }, },};
export function MyForm() { return <GuiForm config={config} />;}import { Component } from '@angular/core';import { CommonModule } from '@angular/common';import { gui } from '@golemui/gui-shared';import { FormComponent } from '@golemui/gui-angular';import { uploadService } from './upload-service';
@Component({ imports: [CommonModule, FormComponent], selector: 'app-my-form', template: `<gui-form [config]="config"></gui-form>`,})export class MyForm { protected config = { formDef: [gui.inputs.multiFileUpload('attachments', { label: 'Attachments' })], formConfig: { dependencies: { uploadService }, }, };}import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import { gui } from '@golemui/gui-shared';import '@golemui/gui-lit';import { uploadService } from './upload-service';
@customElement('my-form')export class MyForm extends LitElement { config = { formDef: [gui.inputs.multiFileUpload('attachments', { label: 'Attachments' })], formConfig: { dependencies: { uploadService }, }, };
override createRenderRoot() { return this; }
override render() { return html`<gui-form .config=${this.config}></gui-form>`; }}<script setup lang="ts">import { gui } from '@golemui/gui-shared';import { GuiForm } from '@golemui/gui-vue';import { uploadService } from './upload-service';
const config = { formDef: [gui.inputs.multiFileUpload('attachments', { label: 'Attachments' })], formConfig: { dependencies: { uploadService }, },};</script>
<template> <GuiForm :config="config" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';import { gui } from '@golemui/gui-shared';import { uploadService } from './upload-service';
const form = document.getElementById('app-form');form.config = { formDef: [gui.inputs.multiFileUpload('attachments', { label: 'Attachments' })], formConfig: { dependencies: { uploadService }, },};In the JSON path, the service goes into the dependencies property of the config object.
{ "form": [ { "kind": "input", "type": "multiFileUpload", "path": "attachments", "label": "Attachments" } ]}import { GuiForm } from '@golemui/gui-react';import formDef from './my-form.json';import { uploadService } from './upload-service';
const config = { formDef, dependencies: { uploadService },};
export function MyForm() { return <GuiForm config={config} />;}import { Component } from '@angular/core';import { CommonModule } from '@angular/common';import { FormComponent } from '@golemui/gui-angular';import formDef from './my-form.json';import { uploadService } from './upload-service';
@Component({ imports: [CommonModule, FormComponent], selector: 'app-my-form', template: `<gui-form [config]="config"></gui-form>`,})export class MyForm { protected config = { formDef, dependencies: { uploadService }, };}import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-lit';import formDef from './my-form.json';import { uploadService } from './upload-service';
@customElement('my-form')export class MyForm extends LitElement { config = { formDef, dependencies: { uploadService }, };
override createRenderRoot() { return this; }
override render() { return html`<gui-form .config=${this.config}></gui-form>`; }}<script setup lang="ts">import { GuiForm } from '@golemui/gui-vue';import formDef from './my-form.json';import { uploadService } from './upload-service';
const config = { formDef, dependencies: { uploadService },};</script>
<template> <GuiForm :config="config" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';import formDef from './my-form.json';import { uploadService } from './upload-service';
const form = document.getElementById('app-form');form.config = { formDef, dependencies: { uploadService },};A fetch-based service
Section titled “A fetch-based service”A minimal service posts each file as FormData and returns the JSON your endpoint answers with, which the widget stores in that file’s data. The remove method uses that response to tell the server which file to delete. The same object serves the File Upload widget; the only difference is that this widget calls it once per file.
import type { UploadService } from '@golemui/gui-shared';
// Module level: the same object on every render.export const uploadService: UploadService = { async upload(file, { path, signal }) { const body = new FormData(); body.append('file', file, file.name); body.append('field', path); const response = await fetch('/api/uploads', { method: 'POST', body, signal, }); if (!response.ok) throw new Error(`Upload failed (${response.status})`); return response.json(); // for example { key: 'u/8f1c', url: 'https://cdn.example.com/u/8f1c' } }, async remove(item) { const { key } = item.data as { key: string }; const response = await fetch(`/api/uploads/${encodeURIComponent(key)}`, { method: 'DELETE', }); if (!response.ok) throw new Error(`Could not delete ${item.name}`); },};fetch cannot report upload progress, so with this service the bar stays at zero until each promise settles. For a moving bar use XMLHttpRequest, forward xhr.upload.onprogress to onProgress, and call xhr.abort() when signal fires.
Cleaning up server files
Section titled “Cleaning up server files”This widget follows the same cleanup contract as File Upload: remove runs only when the user removes a pill. Every other way a file leaves the array (a deleted repeater row, a field pruned because a state hid it, setData or a config replacement, a form the user abandons) keeps the file on the server. Treat every upload result as provisional and reconcile your server files against the submitted value.
Not configured
Section titled “Not configured”Without an uploadService the widget does not break the form. The box renders disabled with an inline message, File uploads are not configured by default, and logs a console.error naming the widget, so a missing dependency is visible instead of swallowing files. Change the wording with missingServiceMessage.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('attachments', { label: 'Attachments', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "attachments", "label": "Attachments" } ]}Use these props to customize your Multi File Upload widget. Every text prop is a Localizable, and the message props accept a {name} token that is replaced with the file name.
| prop | type | description |
|---|---|---|
accept | string[] | Accepted file types, for example ["image/*", ".pdf"]. Checked before uploading, since drag and drop bypasses the picker’s own filter, and passed to the picker as a hint |
acceptMessage | Localizable | Error shown when a file does not match accept. Defaults to File type not accepted |
buttonLabel | Localizable | Text of the upload button inside the box. Defaults to Upload files |
cancelAriaLabel | Localizable | Accessible name of the cancel button shown while uploading. Defaults to Cancel {name} |
failedMessage | Localizable | Screen reader announcement after a failed upload. Defaults to {name} failed to upload. |
hint | string | A description displayed below the box |
icon | string | CSS class string for a leading icon, the same convention used by Textinput |
interruptedMessage | Localizable | Error shown for a restored file whose upload never finished. Defaults to {name} was not uploaded. Remove it and pick the file again. |
maxSize | number | Maximum size per file, in bytes. Larger files are refused before uploading |
maxSizeMessage | Localizable | Error shown when a file exceeds maxSize. Defaults to File exceeds the maximum size |
missingServiceMessage | Localizable | Text shown inside the disabled box when no uploadService is provided. Defaults to File uploads are not configured |
removeAriaLabel | Localizable | Accessible name of the remove button on each pill. Defaults to Remove {name} |
removedMessage | Localizable | Screen reader announcement after a removal. Defaults to {name} removed. |
removeIcon | string | CSS class string for the remove and cancel button icon. When omitted, a built-in icon is rendered |
retryAriaLabel | Localizable | Accessible name of the retry button shown on a failed file. Defaults to Retry {name} |
retryIcon | string | CSS class string for the retry button icon. When omitted, a built-in icon is rendered |
uploadedMessage | Localizable | Screen reader announcement after a successful upload. Defaults to {name} uploaded. |
disabled and readonly are standard widget fields, not props. A disabled widget keeps its box and button but accepts no files, and a readonly widget shows the current pills without the upload button or the remove actions.
Accept and max size
Section titled “Accept and max size”Use the properties accept and maxSize to refuse files before they are uploaded. accept takes the same tokens as the native file input (.pdf, image/*, application/pdf) and is also passed to the picker as a hint, but the widget checks it again on drop because drag and drop bypasses the picker’s filter. maxSize is in bytes and applies to each file separately. A refused file is not dropped silently: it stays in the box with the reason below the input and a remove button, and the files picked after it wait until it is removed. Change the wording with acceptMessage and maxSizeMessage.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('documents', { hint: 'PDF or Word, 5 MB each', accept: ['application/pdf', '.doc', '.docx'], maxSize: 5242880, acceptMessage: 'Only PDF or Word files are accepted', maxSizeMessage: 'Keep each file under 5 MB', label: 'Documents', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "documents", "label": "Documents", "props": { "hint": "PDF or Word, 5 MB each", "accept": ["application/pdf", ".doc", ".docx"], "maxSize": 5242880, "acceptMessage": "Only PDF or Word files are accepted", "maxSizeMessage": "Keep each file under 5 MB" } } ]}Button label and icon
Section titled “Button label and icon”Use the property buttonLabel to change the text of the upload button, and icon to render a leading icon at the start of the box. The icon value is a CSS class string, the same convention used by Textinput.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('receipts', { buttonLabel: 'Add receipts', icon: 'attach_file', label: 'Receipts', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "receipts", "label": "Receipts", "props": { "buttonLabel": "Add receipts", "icon": "attach_file" } } ]}Use the property hint to add a description below the box.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('attachments', { hint: 'Drop several files at once. They upload one at a time', label: 'Attachments', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "attachments", "label": "Attachments", "props": { "hint": "Drop several files at once. They upload one at a time" } } ]}Preloaded values
Section titled “Preloaded values”Use defaultValue, or the form data, to show files that already live on your server. Pass an array of the same envelopes the widget would have produced, with status: "uploaded" and whatever your endpoint returned in data. The widget renders one pill per file and never uploads them again. Removing a pill awaits uploadService.remove like any other file.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('photos', { label: 'Photos', defaultValue: [ { id: 'srv-photo-1', name: 'front.jpg', size: 512000, type: 'image/jpeg', status: 'uploaded', data: { url: 'https://cdn.example.com/uploads/front.jpg', }, }, { id: 'srv-photo-2', name: 'back.jpg', size: 498000, type: 'image/jpeg', status: 'uploaded', data: { url: 'https://cdn.example.com/uploads/back.jpg', }, }, ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "photos", "label": "Photos", "defaultValue": [ { "id": "srv-photo-1", "name": "front.jpg", "size": 512000, "type": "image/jpeg", "status": "uploaded", "data": { "url": "https://cdn.example.com/uploads/front.jpg" } }, { "id": "srv-photo-2", "name": "back.jpg", "size": 498000, "type": "image/jpeg", "status": "uploaded", "data": { "url": "https://cdn.example.com/uploads/back.jpg" } } ] } ]}Interrupted uploads
Section titled “Interrupted uploads”A file restored with status: "uploading", for example from a draft saved while it was still in flight, cannot resume: the File only existed in the session that picked it. The widget shows it as failed with interruptedMessage as the reason, so the user removes it and picks the file again. The stored value is left untouched: no change fires until the user removes the file. Files that had finished keep their pills. Use the property interruptedMessage to change the wording; the {name} token is replaced with the file name.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('drafts', { hint: 'Restored from a saved session: one file had finished, the other was still uploading', interruptedMessage: '{name} did not finish uploading. Remove it and pick the file again.', label: 'Drafts', defaultValue: [ { id: 'srv-brief', name: 'brief.pdf', size: 143360, type: 'application/pdf', status: 'uploaded', data: { url: 'https://cdn.example.com/uploads/brief.pdf', }, }, { id: 'restored-appendix', name: 'appendix.pdf', size: 98304, type: 'application/pdf', status: 'uploading', }, ], }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "drafts", "label": "Drafts", "props": { "hint": "Restored from a saved session: one file had finished, the other was still uploading", "interruptedMessage": "{name} did not finish uploading. Remove it and pick the file again." }, "defaultValue": [ { "id": "srv-brief", "name": "brief.pdf", "size": 143360, "type": "application/pdf", "status": "uploaded", "data": { "url": "https://cdn.example.com/uploads/brief.pdf" } }, { "id": "restored-appendix", "name": "appendix.pdf", "size": 98304, "type": "application/pdf", "status": "uploading" } ] } ]}Custom buttons and announcements
Section titled “Custom buttons and announcements”Each pill has a remove action, and the bar has a cancel button while uploading and remove and retry buttons when a file failed. removeIcon and retryIcon swap the built-in icons for any CSS class string, and removeAriaLabel, cancelAriaLabel and retryAriaLabel set their accessible names, with {name} replaced by the file name. Three more props feed the widget’s live region for screen readers: uploadedMessage, removedMessage and failedMessage. Upload a file whose name contains fail to see the retry button and trigger the failure announcement.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('photos', { hint: 'Pick a file whose name contains "fail" to see the retry button and the failure announcement', accept: ['image/*'], removeAriaLabel: 'Discard {name}', cancelAriaLabel: 'Stop uploading {name}', retryAriaLabel: 'Try {name} again', removeIcon: 'delete', retryIcon: 'refresh', uploadedMessage: '{name} is ready.', removedMessage: '{name} was discarded.', failedMessage: '{name} could not be uploaded.', label: 'Photos', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "photos", "label": "Photos", "props": { "hint": "Pick a file whose name contains \"fail\" to see the retry button and the failure announcement", "accept": ["image/*"], "removeAriaLabel": "Discard {name}", "cancelAriaLabel": "Stop uploading {name}", "retryAriaLabel": "Try {name} again", "removeIcon": "delete", "retryIcon": "refresh", "uploadedMessage": "{name} is ready.", "removedMessage": "{name} was discarded.", "failedMessage": "{name} could not be uploaded." } } ]}Max items
Section titled “Max items”There is no prop that caps the number of files. Use the files validator’s maxItems instead: the extra file is uploaded and stored, and the validator flags it, so the user is told why instead of clicking a button that does nothing. Combine it with messages to change the wording, as in Validation.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('photos', { hint: 'Up to 3 images. A fourth file is flagged by the validator, not blocked', accept: ['image/*'], buttonLabel: 'Upload images', label: 'Photos', validator: { maxItems: 3, messages: { maxItems: 'No more than 3 photos, please', }, }, }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "photos", "label": "Photos", "props": { "hint": "Up to 3 images. A fourth file is flagged by the validator, not blocked", "accept": ["image/*"], "buttonLabel": "Upload images" }, "validator": { "type": "files", "maxItems": 3, "messages": { "maxItems": "No more than 3 photos, please" } } } ]}Validation
Section titled “Validation”The value is an array of file envelopes, so the widget takes the files validator: required, minItems, maxItems and blockPendingUploads. blockPendingUploads defaults to true and fails while any file is still uploading or failed to upload, so a half-finished batch can never be submitted silently. Validation errors surface like any other input: below the box, with aria-invalid wired automatically. Override the default copy per rule with a messages object using the keys invalid, required, minItems, maxItems and pendingUploads; see Features / Validators for the full rule list.
The demo validates on blur. Add one file and tab away to see the minItems message, or tab away while a file is still uploading to see pendingUploads.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiFileUpload('attachments', { hint: 'Between 2 and 5 files', label: 'Attachments', validator: { required: true, minItems: 2, maxItems: 5, messages: { required: 'Please attach at least two files', invalid: 'Please attach at least two files', minItems: 'Attach at least 2 files', maxItems: 'No more than 5 files, please', pendingUploads: 'Wait for every file to finish uploading', }, }, }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiFileUpload", "path": "attachments", "label": "Attachments", "props": { "hint": "Between 2 and 5 files" }, "validator": { "type": "files", "required": true, "minItems": 2, "maxItems": 5, "messages": { "required": "Please attach at least two files", "invalid": "Please attach at least two files", "minItems": "Attach at least 2 files", "maxItems": "No more than 5 files, please", "pendingUploads": "Wait for every file to finish uploading" } } } ]}Styling
Section titled “Styling”Multi File Upload widgets can be styled as explained in the Styling Guide. They share the File Upload styles, and the pills pick up the --gui-pill-* tokens documented on the Tags page.
CSS Variables
Section titled “CSS Variables”Following you will find a list with the CSS Variables and a quick description of what you will style.
| CSS Variable | Description |
|---|---|
--gui-upload-pct | Upload progress, set by the widget. Width of the fill and clip of the text overlay |
--gui-intent-primary | Progress fill color and pill background |
--gui-intent-primary-text | Text color over the progress fill |
--gui-intent-error | Box border when invalid, service error text, remove button hover color |
--gui-border-default | Upload button border, icon and pills separators |
--gui-border-focus | Box border on focus and drag over, upload button hover border, retry button hover color |
--gui-shadow-focus | Box shadow on focus and drag over, action button focus ring |
--gui-shadow-focus-error | Box shadow on focus when invalid |
--gui-bg-default | Upload button hover background |
--gui-bg-surface | Remove, cancel and retry button background |
--gui-bg-disabled | Box background when disabled |
--gui-text-default | Upload button and action button text color |
--gui-text-disabled | Box text color when disabled |
--gui-widget-size | Upload button height, minus 0.75rem |
--gui-radius | Upload button corner radius |
--gui-font-sm | Upload button font size |
--gui-font-xs | Percentage and service error font size |
--gui-space-1 | Upload button start margin |
--gui-space-2 | Gap between the box children and between pills, bar padding |
--gui-space-3 | Upload button horizontal padding |
--gui-transition-normal | Progress fill transition |
Anatomy
Section titled “Anatomy”This is the anatomy of the Multi File Upload Widget in case you want to use your CSS styles.
<gui-multi-file-upload class="gui-field"> <span class="gui-label" id="fieldUid_label"> <span class="gui-label__text">Photos</span> </span>
<div class="gui-widget"> <div role="group" class="gui-widget-input gui-file-upload__box" aria-label="Photos"> <!-- One pill per uploaded file --> <gui-pills class="gui-file-upload__pills"> <div class="gui-pills__strip-wrapper gui-pills--end-shadow gui-pills--start-shadow"> <div class="gui-pills__strip" role="toolbar" tabindex="-1" aria-label="Uploaded files"> <span class="gui-sentinel gui-sentinel__start"></span>
<button type="button" class="gui-pills__pill" tabindex="0" aria-label="front.jpg" aria-description="Remove" > <span class="gui-pills__pill-text">front.jpg</span>
<span class="gui-pills__pill-remove" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true" > <path d="M165.66,101.66,139.31,12…"></path> </svg> </span> </button>
<button type="button" class="gui-pills__pill" tabindex="0" aria-label="back.jpg" aria-description="Remove" > <span class="gui-pills__pill-text">back.jpg</span>
<span class="gui-pills__pill-remove" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true" > <path d="M165.66,101.66,139.31,12…"></path> </svg> </span> </button>
<span class="gui-sentinel gui-sentinel__end"></span> </div> </div>
<!-- Shown instead of the strip when the box is too narrow for every pill --> <div class="gui-pills__compact"> <button type="button" aria-haspopup="true" class="gui-pills__count" aria-label="2 files" aria-expanded="false" aria-controls="fieldUid_pills_dropdown" > 2 </button> </div> </gui-pills>
<!-- While no file is uploading: the upload button --> <button type="button" class="gui-file-upload__button"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true" > <path d="M240,136v64a16,16,0,0,1-…"></path> </svg> <span>Upload files</span> </button>
<!-- While a file uploads (--uploading) or failed (--error): the bar replaces the button, one file at a time --> <div class="gui-file-upload__bar gui-file-upload__bar--uploading" data-status="uploading" style="--gui-upload-pct: 19%" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="19" aria-valuetext="side.jpg, 19%" > <span class="gui-file-upload__fill" aria-hidden="true"></span> <span class="gui-file-upload__text"> <span class="gui-file-upload__text-base" ><span class="gui-file-upload__name">side.jpg</span ><span class="gui-file-upload__pct"> 3/3 · 19% </span></span > <span class="gui-file-upload__text-overlay" aria-hidden="true" ><span class="gui-file-upload__name">side.jpg</span ><span class="gui-file-upload__pct"> 3/3 · 19% </span></span > </span>
<!-- Cancel while uploading; a failed file gets remove and gui-file-upload__action--retry buttons --> <button type="button" class="gui-file-upload__action" aria-label="Cancel side.jpg"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true" > <path d="M165.66,101.66,139.31,12…"></path> </svg> </button> </div>
<input type="file" class="gui-visually-hidden gui-file-upload__input" tabindex="-1" id="fieldUid" multiple="" /> </div> </div>
<div class="gui-visually-hidden" role="status" aria-live="polite"></div>
<ul class="gui-validator gui-validator--empty" id="fieldUid_errors" role="alert"></ul></gui-multi-file-upload>