Skip to content
Components, switch product

Multiple file upload

<gui-multi-file-upload> is a field for several files. The user picks or drops any number of files, the element uploads them one after another through the upload service you provide, and lists the uploaded files as pills the user can remove. It works like the file upload, for a list of files.

Code of the example above
MultiFileUploadBasic.tsx
import { GuiMultiFileUpload } from '@golemui/gui-components/react';
import type { UploadService } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
// A stand-in for your upload endpoint: it reports progress and resolves after a second.
const uploadService: UploadService = {
upload(file, { onProgress, signal }) {
return new Promise((resolve, reject) => {
let progress = 0;
const timer = setInterval(() => {
progress += 20;
onProgress?.(progress);
if (progress === 100) {
clearInterval(timer);
resolve({ url: `/uploads/${encodeURIComponent(file.name)}` });
}
}, 200);
signal.addEventListener('abort', () => {
clearInterval(timer);
reject(signal.reason);
});
});
},
};
const dependencies = { uploadService };
export function MultiFileUploadBasic() {
return (
<GuiMultiFileUpload
id="attachments"
label="Attachments"
hint="Add as many files as you need."
dependencies={dependencies}
/>
);
}

The examples on this page use a stand-in upload service that sends nothing: it waits a moment and resolves. Try a few files at once.

The element’s entry point is @golemui/gui-components/multi-file-upload, which registers <gui-multi-file-upload> and nothing else. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

MultiFileUploadExample.tsx
import { GuiMultiFileUpload } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function MultiFileUploadExample() {
return (
<GuiMultiFileUpload
label="Attachments"
name="attachments"
/>
);
}

The element’s value is its values property, the list of files. Bind your state to values and update it from gui-change, whose detail.value is the new list. The element also needs your upload service in its dependencies property: bind it as a property, the way your framework passes an object to an element.

Properties

Property Type Default Description
values FileItem[] [] The files, with their upload status.
path string – The form data path, forwarded to uploadService.upload as ctx.path.
dependencies { uploadService?: UploadService } – Provides the uploadService that uploads and removes files.
icon string – Icon class name of the upload button.
accept string[] – Accepted file types, as MIME types (image/*) or extensions (.pdf). The attribute takes the native comma list (accept=".pdf,image/*") or a JSON array.
maxSize attribute max-size number – Largest accepted file, in bytes.
buttonLabel attribute button-label string – Text of the upload button.
removeAriaLabel attribute remove-aria-label string – Accessible name of the remove button. {name} is the file name. An empty value keeps the default.
cancelAriaLabel attribute cancel-aria-label string – Accessible name of the cancel button of an upload in progress. {name} is the file name. An empty value keeps the default.
retryAriaLabel attribute retry-aria-label string – Accessible name of the retry button. {name} is the file name. An empty value keeps the default.
removeIcon attribute remove-icon string – Icon class name of the remove button, replacing the default ×.
retryIcon attribute retry-icon string – Icon class name of the retry button.
maxSizeMessage attribute max-size-message string – Error for a file larger than maxSize.
acceptMessage attribute accept-message string – Error for a file whose type is not in accept.
interruptedMessage attribute interrupted-message string – Error for a file whose upload never finished. {name} is the file name.
missingServiceMessage attribute missing-service-message string – Text shown when no uploadService is provided.
uploadedMessage attribute uploaded-message string – Announcement after a file uploads. {name} is the file name.
removedMessage attribute removed-message string – Announcement after a file is removed. {name} is the file name.
failedMessage attribute failed-message string – Announcement after an upload fails. {name} is the file name.
pendingMessage attribute pending-message string – Error that blocks a native form submit while a file is still uploading.
uploadErrorMessage attribute upload-error-message string – Error that blocks a native form submit while a file failed to upload. {name} is the file name.
uid string – The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration.

Form control properties

Every field has these, from the base class of the form controls.

Property Type Default Description
value FileItem | null null The file, with its upload status, or null.
name string – The name the value is submitted under. Without it the element stays out of its form. Reflected, like a native control's: the form reads a single value's name from the attribute.
label string – The visible label, which also names the control for assistive technology. Without one the control has no accessible name; the date, time, range, tags and file fields warn about it in development builds.
hint string – Help text shown under the label and announced as the control's description.
errors string[] [] Error messages to show, typically from your own validation. They show as soon as they are set, unless touched is false.
touched boolean – Whether the user has interacted with the control. Leave it unset unless you validate on interaction: false holds errors back until it becomes true.
required boolean false The control needs a value: it is marked as required and, with a name, blocks submission.
readOnly attribute readonly boolean – The value can be read and focused but not changed.
disabled boolean – Disabled by its own attribute or by a disabled <fieldset> around it. Setting the property sets the attribute, like a native control's.
form read-only, no attribute HTMLFormElement | null – The <form> the element belongs to, if any.
validity read-only, no attribute ValidityState – The element's validity, like a native control's.
validationMessage read-only, no attribute string – The message the browser shows for an invalid value.

Methods

Method Description
checkValidity(): boolean –
reportValidity(): boolean –

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-input The files changed. detail.value is the list of files.
gui-change The files changed: one was added, uploaded or removed. detail.value is the list of files.
gui-blur Focus left the control.
gui-input-error A file was refused (its type or size), its upload failed or removing it failed. detail.message is the error, or '' when it clears; show it through errors.

CSS custom properties

Property Default Description
--gui-pill-height – Height of each pill.
--gui-pill-font-size – Font size of the pill text.
--gui-pill-action-size – Size of the icons inside a pill.
--gui-pill-action-hit – Clickable area of the buttons inside a pill.

The inherited value property isn’t used: the files are in values.

The upload service is the same object as for the file upload: an upload(file, context) method that sends one file and resolves with what your server returns, and an optional remove(item) method. It’s described, with an example, in The upload service.

values is a list with one entry per file, each the same description of a file as the file upload’s value: id, name, size, type, status, and error or data. See The value.

  • One at a time. The element uploads the files in the order the user added them, one after another. The bar shows the file in progress, with its position and percentage, such as 2/3 · 60%.
  • A failed file pauses the queue. The files after it wait until the user retries or removes it.
  • Uploaded files show as pills. The user removes one with its ×, or with Delete or Backspace while it has focus. The element calls your remove and waits for it.

The element fires gui-input and gui-change each time the list changes: when the user adds files, when each file finishes uploading or fails, and when the user removes one.

accept lists the file types the field takes, as MIME types such as image/* or extensions such as .pdf, and max-size is the largest size of each file, in bytes. A file that doesn’t match is refused without uploading, and pauses the queue like a failed upload until the user removes it.

Show the element’s gui-input-error messages through errors, as below. Try a few images together with a PDF.

Code of the example above
MultiFileUploadLimits.tsx
import { useState } from 'react';
import { GuiMultiFileUpload } from '@golemui/gui-components/react';
import type { UploadService } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
// A stand-in for your upload endpoint, which resolves after a second.
const uploadService: UploadService = {
upload: (file) =>
new Promise((resolve) =>
setTimeout(() => resolve({ url: `/gallery/${file.name}` }), 1000),
),
};
const dependencies = { uploadService };
const accept = ['image/*'];
export function MultiFileUploadLimits() {
const [errors, setErrors] = useState<string[]>([]);
return (
<GuiMultiFileUpload
id="gallery"
label="Gallery"
hint="Images only, up to 2 MB each."
accept={accept}
maxSize={2097152}
acceptMessage="Only images can go in the gallery."
maxSizeMessage="Each image must be 2 MB or less."
dependencies={dependencies}
errors={errors}
onGuiInputError={(event) =>
setErrors(event.detail.message ? [event.detail.message] : [])
}
/>
);
}

Set values to files uploaded earlier and the field lists them as pills. New files add to the list. When the user removes a file, the element calls your service’s remove: remove a pill below.

Code of the example above
MultiFileUploadExisting.tsx
import { useMemo, useState } from 'react';
import { GuiMultiFileUpload } from '@golemui/gui-components/react';
import type { FileItem, UploadService } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
// Files uploaded earlier, as your server returned them.
const caseFiles: FileItem<{ url: string }>[] = [
{
id: 'f_statement',
name: 'statement.pdf',
size: 81920,
type: 'application/pdf',
status: 'uploaded',
data: { url: '/cases/statement.pdf' },
},
{
id: 'f_receipt',
name: 'receipt.jpg',
size: 153600,
type: 'image/jpeg',
status: 'uploaded',
data: { url: '/cases/receipt.jpg' },
},
];
export function MultiFileUploadExisting() {
const [files, setFiles] = useState<FileItem[]>(caseFiles);
const [log, setLog] = useState('');
const dependencies = useMemo(() => {
const uploadService: UploadService = {
// Stand-ins for your endpoints: upload resolves after a second, remove at once.
upload: (file) =>
new Promise((resolve) =>
setTimeout(() => resolve({ url: `/cases/${file.name}` }), 1000),
),
remove: async (item: FileItem<{ url: string }>) => {
setLog(`Deleted ${item.data?.url} on the server.`);
},
};
return { uploadService };
}, []);
return (
<>
<GuiMultiFileUpload
id="case-files"
label="Case files"
dependencies={dependencies}
values={files}
onGuiChange={(event) => setFiles(event.detail.value ?? [])}
/>
<p id="case-files-log" role="status">
{log}
</p>
</>
);
}

With a name, <gui-multi-file-upload> submits one entry per file under that name, each the file’s description as JSON. Read them all with FormData.getAll(). required blocks the submit while the list is empty.

A file that is still uploading, or that failed, blocks the submit, as in the file upload. Submit before the uploads finish to see it.

Code of the example above
MultiFileUploadNativeForm.tsx
import { useState, type SubmitEvent } from 'react';
import { GuiButton, GuiMultiFileUpload } from '@golemui/gui-components/react';
import type { FileItem, UploadService } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
// A stand-in for your upload endpoint, which resolves after a second.
const uploadService: UploadService = {
upload: (file) =>
new Promise((resolve) =>
setTimeout(() => resolve({ url: `/receipts/${file.name}` }), 1000),
),
};
const dependencies = { uploadService };
export function MultiFileUploadNativeForm() {
const [result, setResult] = useState('');
// The browser runs this only once every receipt has uploaded.
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
// One entry per file, each a JSON string.
const files: FileItem<{ url: string }>[] = new FormData(event.currentTarget)
.getAll('receipts')
.map((entry) => JSON.parse(String(entry)));
setResult(`Submitted ${files.map((file) => file.data?.url).join(', ')}`);
};
return (
<form id="expense-report" onSubmit={onSubmit}>
<GuiMultiFileUpload
id="expense-receipts"
label="Receipts"
name="receipts"
required
dependencies={dependencies}
/>
<GuiButton type="submit" label="Send report" />
<p id="expense-report-result" role="status">
{result}
</p>
</form>
);
}

How the elements submit, validate and reset is in Native forms.

<gui-multi-file-upload> renders into the light DOM, with the parts of the file upload and a strip of pills, a gui-pills element, for the uploaded files. With two files uploaded and a third in progress:

gui-multi-file-upload
<gui-multi-file-upload class="gui-field">
<span class="gui-label" id="attachments_label">
<span class="gui-label__text">Attachments</span>
</span>
<div class="gui-widget">
<div
class="gui-widget-input gui-file-upload__box"
role="group"
aria-labelledby="attachments_label"
>
<gui-pills class="gui-file-upload__pills">
<div class="gui-pills__strip-wrapper">
<div
class="gui-pills__strip"
role="toolbar"
aria-label="Uploaded files"
>
<button
type="button"
class="gui-pills__pill"
aria-label="minutes.pdf"
aria-description="Remove"
>
<span class="gui-pills__pill-text">minutes.pdf</span>
<span class="gui-pills__pill-remove" aria-hidden="true"
><svg>…</svg></span
>
</button>
<!-- …one pill per uploaded file -->
</div>
</div>
<div class="gui-pills__compact">
<button
type="button"
class="gui-pills__count"
aria-haspopup="true"
aria-label="2 files"
>
2
</button>
</div>
</gui-pills>
<div
class="gui-file-upload__bar gui-file-upload__bar--uploading"
role="progressbar"
aria-valuetext="logo.png, 60%"
>
<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">logo.png</span>
<span class="gui-file-upload__pct">3/3 · 60%</span>
</span>
<span class="gui-file-upload__text-overlay" aria-hidden="true"
>…</span
>
</span>
<button
type="button"
class="gui-file-upload__action"
aria-label="Cancel logo.png"
>
<svg aria-hidden="true">…</svg>
</button>
</div>
<input
type="file"
class="gui-visually-hidden gui-file-upload__input"
multiple
/>
</div>
</div>
<div class="gui-visually-hidden" role="status" aria-live="polite"></div>
<div class="gui-validator" id="attachments_errors" role="alert"></div>
</gui-multi-file-upload>

Once no file is uploading, the upload button, labelled “Upload files”, takes the place of the bar. When the field is 540px wide or less, the count button shows instead of the pills, and opens a list of the files.

ClassPart
gui-file-upload__pillsThe gui-pills strip of uploaded files.
gui-pills__pillOne uploaded file.
gui-pills__pill-textThe file name in a pill.
gui-pills__pill-removeThe × of a pill.
gui-pills__countThe count of files, which shows in place of the pills in a narrow field.
gui-file-upload__*The field, the bar and the buttons, as in the file upload.

The pills are a gui-pills element, documented with the tags field.

The field reads the same design tokens as the file upload, and the pill tokens listed in its API above, --gui-pill-height, --gui-pill-font-size, --gui-pill-action-size and --gui-pill-action-hit, with their defaults in Tokens. The pills also read:

TokenWhat it styles
--gui-intent-primaryThe background of the pills and of the count.
--gui-color-whiteThe text of the pills.
--gui-radius-mdThe corners of the pills.
--gui-radius-fullThe round count.
--gui-shadow-focusThe focus ring of a pill.
--gui-border-defaultThe line between the pills and the upload button.
styles.css
/* Taller pills for touch screens. */
.mobile-form {
--gui-pill-height: 32px;
--gui-pill-action-hit: 32px;
}

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Name. The label names the field, a group around the pills, the bar and the upload button. Without a label the field has no accessible name, and development builds log a warning.
  • Pills. The pills sit in a toolbar named “Uploaded files”. Each pill is a button named after its file, with “Remove” as its description.
  • Progress and announcements. As in the file upload: the bar is a progress bar while a file uploads, and screen readers hear when each file uploads, fails or is removed.
  • Buttons. The cancel, retry and remove buttons are named after the file, such as “Cancel logo.png”. cancel-aria-label, retry-aria-label and remove-aria-label change them.
  • Focus. After the user removes a file, focus moves to the upload button.
KeyAction
TabMoves focus through the pills, the file’s buttons and the upload button.
← / →Moves focus to the previous or next pill.
Home / EndMoves focus to the first or last pill.
Delete or BackspaceRemoves the focused pill’s file.
Enter or SpaceActivates the focused button: opens the file picker, cancels, retries or removes.

The default strings are in English; translate them with the keys listed on the file upload, plus uploadFiles, uploadedFiles and fileCount. See i18n. What every element shares is in Accessibility.