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.
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';
// 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); }); }); },};
@Component({ selector: 'app-multi-file-upload-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-multi-file-upload id="attachments" label="Attachments" hint="Add as many files as you need." [dependencies]="dependencies" ></gui-multi-file-upload> `,})export class MultiFileUploadBasicComponent { dependencies = { uploadService };}<script setup lang="ts">import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';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 };</script>
<template> <gui-multi-file-upload id="attachments" label="Attachments" hint="Add as many files as you need." :dependencies.prop="dependencies" ></gui-multi-file-upload></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';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 };
@customElement('multi-file-upload-basic')export class MultiFileUploadBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-multi-file-upload id="attachments" label="Attachments" hint="Add as many files as you need." .dependencies=${dependencies} ></gui-multi-file-upload> `; }}<gui-multi-file-upload id="attachments" label="Attachments" hint="Add as many files as you need."></gui-multi-file-upload>
<script type="module"> // A stand-in for your upload endpoint: it reports progress and resolves after a second. const 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); }); }); }, };
document.querySelector('#attachments').dependencies = { uploadService };</script>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.
import { GuiMultiFileUpload } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function MultiFileUploadExample() { return ( <GuiMultiFileUpload label="Attachments" name="attachments" /> );}Add CUSTOM_ELEMENTS_SCHEMA to the component that uses the tags, and the stylesheet to styles in angular.json.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/multi-file-upload';
@Component({ selector: 'app-multi-file-upload-example', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-multi-file-upload label="Attachments" name="attachments" ></gui-multi-file-upload> `,})export class MultiFileUploadExampleComponent {}"styles": ["node_modules/@golemui/gui-components/lib/styles/index.css"]Tell Vue's compiler that gui-* tags are custom elements, and add the typings once for typed props and @gui-* handlers.
<script setup lang="ts">import '@golemui/gui-components/multi-file-upload';import '@golemui/gui-components/index.css';</script>
<template> <gui-multi-file-upload label="Attachments" name="attachments" /></template>vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') } },});import type {} from '@golemui/gui-components/vue';The page's stylesheet styles the gui-* elements, so it only reaches them outside a shadow root. Render the host component into the light DOM too, as below, or load the stylesheet into its shadow root.
import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/multi-file-upload';import '@golemui/gui-components/index.css';
@customElement('multi-file-upload-example')export class MultiFileUploadExample extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-multi-file-upload label="Attachments" name="attachments" ></gui-multi-file-upload> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-multi-file-upload label="Attachments" name="attachments"></gui-multi-file-upload>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/multi-file-upload';import '@golemui/gui-components/index.css';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 and the files
Section titled “The upload service and the files”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
removeand 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.
Examples
Section titled “Examples”Accepted files
Section titled “Accepted files”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.
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] : []) } /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiMultiFileUploadEvents, UploadService,} from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';
type FileUploadEvent<K extends keyof typeof GuiMultiFileUploadEvents> = GuiEventMap<typeof GuiMultiFileUploadEvents>[K];
// 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), ),};
@Component({ selector: 'app-multi-file-upload-limits', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-multi-file-upload id="gallery" label="Gallery" hint="Images only, up to 2 MB each." accept="image/*" max-size="2097152" accept-message="Only images can go in the gallery." max-size-message="Each image must be 2 MB or less." [dependencies]="dependencies" [errors]="errors()" (gui-input-error)="onInputError($event)" ></gui-multi-file-upload> `,})export class MultiFileUploadLimitsComponent { dependencies = { uploadService }; errors = signal<string[]>([]);
onInputError(event: Event) { const { message } = (event as FileUploadEvent<'gui-input-error'>).detail; this.errors.set(message ? [message] : []); }}<script setup lang="ts">import { ref } from 'vue';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';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/*'];
const errors = ref<string[]>([]);</script>
<template> <gui-multi-file-upload id="gallery" label="Gallery" hint="Images only, up to 2 MB each." :accept.prop="accept" :maxSize="2097152" accept-message="Only images can go in the gallery." max-size-message="Each image must be 2 MB or less." :dependencies.prop="dependencies" :errors.prop="errors" @gui-input-error=" errors = $event.detail.message ? [$event.detail.message] : [] " ></gui-multi-file-upload></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiMultiFileUploadEvents, UploadService,} from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';import '@golemui/gui-components/index.css';
type MultiFileUploadEvent<K extends keyof typeof GuiMultiFileUploadEvents> = GuiEventMap<typeof GuiMultiFileUploadEvents>[K];
// 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 };
@customElement('multi-file-upload-limits')export class MultiFileUploadLimits extends LitElement { @state() private errors: string[] = [];
protected override createRenderRoot() { return this; }
private onInputError(event: MultiFileUploadEvent<'gui-input-error'>) { this.errors = event.detail.message ? [event.detail.message] : []; }
override render() { return html` <gui-multi-file-upload id="gallery" label="Gallery" hint="Images only, up to 2 MB each." accept="image/*" max-size="2097152" accept-message="Only images can go in the gallery." max-size-message="Each image must be 2 MB or less." .dependencies=${dependencies} .errors=${this.errors} @gui-input-error=${this.onInputError} ></gui-multi-file-upload> `; }}<gui-multi-file-upload id="gallery" label="Gallery" hint="Images only, up to 2 MB each." accept="image/*" max-size="2097152" accept-message="Only images can go in the gallery." max-size-message="Each image must be 2 MB or less."></gui-multi-file-upload>
<script type="module"> const gallery = document.querySelector('#gallery');
// A stand-in for your upload endpoint, which resolves after a second. gallery.dependencies = { uploadService: { upload: (file) => new Promise((resolve) => setTimeout(() => resolve({ url: `/gallery/${file.name}` }), 1000)), }, };
gallery.addEventListener('gui-input-error', (event) => { gallery.errors = event.detail.message ? [event.detail.message] : []; });</script>Existing files
Section titled “Existing files”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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';
@Component({ selector: 'app-multi-file-upload-existing', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-multi-file-upload id="case-files" label="Case files" [dependencies]="dependencies" [values]="files" ></gui-multi-file-upload> <p id="case-files-log" role="status">{{ log() }}</p> `,})export class MultiFileUploadExistingComponent { log = signal('');
private 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 }>) => { this.log.set(`Deleted ${item.data?.url} on the server.`); }, };
dependencies = { uploadService: this.uploadService };
// Files uploaded earlier, as your server returned them. files: FileItem[] = [ { 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' }, }, ];}<script setup lang="ts">import { ref } from 'vue';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';import '@golemui/gui-components/index.css';
const log = ref('');
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 }>) => { log.value = `Deleted ${item.data?.url} on the server.`; },};const dependencies = { uploadService };
// Files uploaded earlier, as your server returned them.const files: FileItem[] = [ { 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' }, },];</script>
<template> <gui-multi-file-upload id="case-files" label="Case files" :dependencies.prop="dependencies" :values.prop="files" ></gui-multi-file-upload> <p id="case-files-log" role="status">{{ log }}</p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/multi-file-upload';import '@golemui/gui-components/index.css';
// Files uploaded earlier, as your server returned them.const files: FileItem[] = [ { 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' }, },];
@customElement('multi-file-upload-existing')export class MultiFileUploadExisting extends LitElement { @state() private log = '';
private 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 }>) => { this.log = `Deleted ${item.data?.url} on the server.`; }, };
private dependencies = { uploadService: this.uploadService };
protected override createRenderRoot() { return this; }
override render() { return html` <gui-multi-file-upload id="case-files" label="Case files" .dependencies=${this.dependencies} .values=${files} ></gui-multi-file-upload> <p id="case-files-log" role="status">${this.log}</p> `; }}<gui-multi-file-upload id="case-files" label="Case files"></gui-multi-file-upload><p id="case-files-log" role="status"></p>
<script type="module"> const caseFiles = document.querySelector('#case-files'); const log = document.querySelector('#case-files-log');
caseFiles.dependencies = { 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) => { log.textContent = `Deleted ${item.data.url} on the server.`; }, }, };
// Files uploaded earlier, as your server returned them. caseFiles.values = [ { 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' }, }, ];</script>In a form
Section titled “In a form”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/multi-file-upload';
// 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), ),};
@Component({ selector: 'app-multi-file-upload-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="expense-report" #form (submit)="onSubmit($event, form)"> <gui-multi-file-upload id="expense-receipts" label="Receipts" name="receipts" required [dependencies]="dependencies" ></gui-multi-file-upload> <gui-button type="submit" label="Send report"></gui-button> <p id="expense-report-result" role="status">{{ result() }}</p> </form> `,})export class MultiFileUploadNativeFormComponent { dependencies = { uploadService }; result = signal('');
// The browser runs this only once every receipt has uploaded. onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); // One entry per file, each a JSON string. const files: FileItem<{ url: string }>[] = new FormData(form) .getAll('receipts') .map((entry) => JSON.parse(String(entry))); this.result.set( `Submitted ${files.map((file) => file.data?.url).join(', ')}`, ); }}<script setup lang="ts">import { ref } from 'vue';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/multi-file-upload';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 };
const result = ref('');
// The browser runs this only once every receipt has uploaded.function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; // 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))); result.value = `Submitted ${files.map((file) => file.data?.url).join(', ')}`;}</script>
<template> <form id="expense-report" @submit.prevent="onSubmit"> <gui-multi-file-upload id="expense-receipts" label="Receipts" name="receipts" :required="true" :dependencies.prop="dependencies" ></gui-multi-file-upload> <gui-button type="submit" label="Send report"></gui-button> <p id="expense-report-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/multi-file-upload';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 };
@customElement('multi-file-upload-native-form')export class MultiFileUploadNativeForm extends LitElement { @query('#expense-report') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
// The browser runs this only once every receipt has uploaded. private onSubmit(event: SubmitEvent) { event.preventDefault(); // One entry per file, each a JSON string. const files: FileItem<{ url: string }>[] = new FormData(this.form) .getAll('receipts') .map((entry) => JSON.parse(String(entry))); this.result = `Submitted ${files.map((file) => file.data?.url).join(', ')}`; }
override render() { return html` <form id="expense-report" @submit=${this.onSubmit}> <gui-multi-file-upload id="expense-receipts" label="Receipts" name="receipts" required .dependencies=${dependencies} ></gui-multi-file-upload> <gui-button type="submit" label="Send report"></gui-button> <p id="expense-report-result" role="status">${this.result}</p> </form> `; }}<form id="expense-report"> <gui-multi-file-upload id="expense-receipts" label="Receipts" name="receipts" required ></gui-multi-file-upload> <gui-button type="submit" label="Send report"></gui-button> <p id="expense-report-result" role="status"></p></form>
<script type="module"> const receipts = document.querySelector('#expense-receipts');
// A stand-in for your upload endpoint, which resolves after a second. receipts.dependencies = { uploadService: { upload: (file) => new Promise((resolve) => setTimeout(() => resolve({ url: `/receipts/${file.name}` }), 1000)), }, };
const form = document.querySelector('#expense-report'); const result = document.querySelector('#expense-report-result');
// The browser runs this only once every receipt has uploaded. form.addEventListener('submit', (event) => { event.preventDefault(); // One entry per file, each a JSON string. const files = new FormData(form).getAll('receipts').map((entry) => JSON.parse(entry)); result.textContent = `Submitted ${files.map((file) => file.data.url).join(', ')}`; });</script>How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<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 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.
| Class | Part |
|---|---|
gui-file-upload__pills | The gui-pills strip of uploaded files. |
gui-pills__pill | One uploaded file. |
gui-pills__pill-text | The file name in a pill. |
gui-pills__pill-remove | The × of a pill. |
gui-pills__count | The 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.
Tokens
Section titled “Tokens”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:
| Token | What it styles |
|---|---|
--gui-intent-primary | The background of the pills and of the count. |
--gui-color-white | The text of the pills. |
--gui-radius-md | The corners of the pills. |
--gui-radius-full | The round count. |
--gui-shadow-focus | The focus ring of a pill. |
--gui-border-default | The line between the pills and the upload button. |
/* 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.
Accessibility
Section titled “Accessibility”- Name. The
labelnames 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-labelandremove-aria-labelchange them. - Focus. After the user removes a file, focus moves to the upload button.
| Key | Action |
|---|---|
| Tab | Moves focus through the pills, the file’s buttons and the upload button. |
| ← / → | Moves focus to the previous or next pill. |
| Home / End | Moves focus to the first or last pill. |
| Delete or Backspace | Removes the focused pill’s file. |
| Enter or Space | Activates 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.