File upload
<gui-file-upload> is a field for one file. The user picks a file or drops it on the field, and
the element uploads it at once through the upload service you provide, with a progress bar, and
the options to cancel, retry or remove it. Its value is a small description of the file, not the
file itself.
import { GuiFileUpload } 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 FileUploadBasic() { return ( <GuiFileUpload id="resume" label="Résumé" hint="A PDF works best." dependencies={dependencies} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/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-file-upload-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-file-upload id="resume" label="Résumé" hint="A PDF works best." [dependencies]="dependencies" ></gui-file-upload> `,})export class FileUploadBasicComponent { dependencies = { uploadService };}<script setup lang="ts">import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/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-file-upload id="resume" label="Résumé" hint="A PDF works best." :dependencies.prop="dependencies" ></gui-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/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('file-upload-basic')export class FileUploadBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-file-upload id="resume" label="Résumé" hint="A PDF works best." .dependencies=${dependencies} ></gui-file-upload> `; }}<gui-file-upload id="resume" label="Résumé" hint="A PDF works best."></gui-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('#resume').dependencies = { uploadService };</script>The examples on this page use a stand-in upload service that sends nothing: it waits a moment and resolves. Try any file.
The element’s entry point is @golemui/gui-components/file-upload, which registers
<gui-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 { useState } from 'react';import { GuiFileUpload } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function ResumeField() { const [resume, setResume] = useState(null);
return ( <GuiFileUpload label="Résumé" name="resume" value={resume} onGuiChange={(event) => setResume(event.detail.value ?? null)} /> );}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 type { GuiEventMap, GuiFileUploadEvents } from '@golemui/gui-components';import '@golemui/gui-components/file-upload';
type ChangeEvent = GuiEventMap<typeof GuiFileUploadEvents>['gui-change'];
@Component({ selector: 'app-resume-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-file-upload label="Résumé" name="resume" [value]="resume" (gui-change)="onChange($event)" ></gui-file-upload> `,})export class ResumeFieldComponent { resume = null;
onChange(event: Event) { this.resume = (event as ChangeEvent).detail.value ?? null; }}"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 { ref } from 'vue';import '@golemui/gui-components/file-upload';import '@golemui/gui-components/index.css';
const resume = ref(null);</script>
<template> <gui-file-upload label="Résumé" name="resume" :value="resume" @gui-change="resume = $event.detail.value ?? null" /></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, state } from 'lit/decorators.js';import '@golemui/gui-components/file-upload';import '@golemui/gui-components/index.css';
@customElement('resume-field')export class ResumeField extends LitElement { @state() private resume = null;
protected override createRenderRoot() { return this; }
override render() { return html` <gui-file-upload label="Résumé" name="resume" .value=${this.resume} @gui-change=${(event: CustomEvent) => (this.resume = event.detail.value)} ></gui-file-upload> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-file-upload label="Résumé" name="resume"></gui-file-upload>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/file-upload';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-file-upload');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});The element also needs your upload service in its dependencies property, below. Bind it as a
property, the way your framework passes an object to an element. Without one, the field is
disabled and says “File uploads are not configured”.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
path | string | – | The form data path, forwarded to uploadService.upload as ctx.path. |
value | FileItem | null | null | The file, with its upload status, or null. |
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 |
|---|---|---|---|
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 user changed the value. detail.value is the new value. |
gui-change | The file changed: it was added, uploaded or removed. detail.value is the file, or null. |
gui-blur | Focus left the control. |
gui-input-error | A file was refused (its type or size) or its upload failed. detail.message is the error, or '' when it clears; show it through errors. |
The upload service
Section titled “The upload service”The element doesn’t send files anywhere by itself. You give it an object with an upload method,
which sends the file to your server, and an optional remove method, in
dependencies.uploadService:
export const uploadService = { // Sends the file. Resolve with what your server returns: it becomes the value's `data`. upload(file, { onProgress, signal }) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open('POST', '/api/uploads'); request.upload.onprogress = (event) => onProgress((event.loaded / event.total) * 100); request.onload = () => request.status < 300 ? resolve(JSON.parse(request.responseText)) : reject(new Error('The upload failed. Try again.')); request.onerror = () => reject(new Error('Check your connection and try again.')); signal.addEventListener('abort', () => request.abort());
const body = new FormData(); body.append('file', file); request.send(body); }); },
// Optional: deletes a file the user removes. async remove(item) { const response = await fetch(`/api/uploads/${item.data.id}`, { method: 'DELETE', }); if (!response.ok) throw new Error('Could not delete the file. Try again.'); },};import { uploadService } from './upload-service.js';
document.querySelector('gui-file-upload').dependencies = { uploadService };upload(file, context)runs as soon as the user picks a file.fileis the browser’sFile.contexthasonProgress, to report a percentage from 0 to 100,signal, anAbortSignalthat aborts when the user cancels the upload, drops another file in its place or the element leaves the page,id, the id of the file in the value, andpath, the element’spath.- What it resolves with is stored as is in the value’s
data, such as the id or URL your server gave the file. If it rejects, the file fails, with the error’s message. remove(item)runs when the user removes an uploaded file, and the element waits for it. If it rejects, the file stays, with the error’s message. When the user drops another file in place of an uploaded one, the element callsremovefor the old one without waiting.- Files the user leaves behind, such as when they never submit the form, stay on your server. Treat every upload as provisional until the form is submitted, and clean up the rest on the server.
- Keep the same object: define the service once, at module level, rather than in a render function.
The value
Section titled “The value”The value is a plain object that describes the file, or null. The File itself never enters
it, so the value is JSON you can store and send:
| Field | What it holds |
|---|---|
id | An id the element gives the file when the user picks it. |
name | The file name. |
size | The size, in bytes. |
type | The MIME type the browser reports, such as application/pdf. |
status | uploading, uploaded or error. |
error | Why the file failed, while status is error. |
data | What your upload resolved with, once status is uploaded. |
The element fires gui-input and gui-change each time the value changes: when the user picks a
file, when it finishes uploading or fails, and when the user removes it.
Examples
Section titled “Examples”Accepted files
Section titled “Accepted files”accept lists the file types the field takes, as MIME types such as image/png or image/*, or
extensions such as .pdf, in a comma list like the native attribute. max-size is the largest
size, in bytes. The element checks both, for picked and dropped files alike, and refuses a file
that doesn’t match without uploading it. accept-message and max-size-message set the error.
The element reports a refused file, and a failed upload, with gui-input-error: show its
detail.message through errors, as below. An empty message means the error is gone. Try a file
that isn’t an image, or an image over 1 MB.
import { useState } from 'react';import { GuiFileUpload } 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: `/avatars/${file.name}` }), 1000), ),};const dependencies = { uploadService };const accept = ['image/png', 'image/jpeg'];
export function FileUploadLimits() { const [errors, setErrors] = useState<string[]>([]);
return ( <GuiFileUpload id="avatar" label="Profile photo" hint="A PNG or JPEG, up to 1 MB." accept={accept} maxSize={1048576} acceptMessage="Pick a PNG or JPEG image." maxSizeMessage="Pick an image of 1 MB or less." dependencies={dependencies} errors={errors} // The element reports a refused file or a failed upload: show it as an error. onGuiInputError={(event) => setErrors(event.detail.message ? [event.detail.message] : []) } /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiFileUploadEvents, UploadService,} from '@golemui/gui-components';import '@golemui/gui-components/file-upload';
type FileUploadEvent<K extends keyof typeof GuiFileUploadEvents> = GuiEventMap< typeof GuiFileUploadEvents>[K];
// A stand-in for your upload endpoint, which resolves after a second.const uploadService: UploadService = { upload: (file) => new Promise((resolve) => setTimeout(() => resolve({ url: `/avatars/${file.name}` }), 1000), ),};
@Component({ selector: 'app-file-upload-limits', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-file-upload id="avatar" label="Profile photo" hint="A PNG or JPEG, up to 1 MB." accept="image/png,image/jpeg" max-size="1048576" accept-message="Pick a PNG or JPEG image." max-size-message="Pick an image of 1 MB or less." [dependencies]="dependencies" [errors]="errors()" (gui-input-error)="onInputError($event)" ></gui-file-upload> `,})export class FileUploadLimitsComponent { dependencies = { uploadService }; errors = signal<string[]>([]);
// The element reports a refused file or a failed upload: show it as an error. 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/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: `/avatars/${file.name}` }), 1000), ),};const dependencies = { uploadService };const accept = ['image/png', 'image/jpeg'];
const errors = ref<string[]>([]);</script>
<template> <!-- The element reports a refused file or a failed upload: show it as an error. --> <gui-file-upload id="avatar" label="Profile photo" hint="A PNG or JPEG, up to 1 MB." :accept.prop="accept" :maxSize="1048576" accept-message="Pick a PNG or JPEG image." max-size-message="Pick an image of 1 MB or less." :dependencies.prop="dependencies" :errors.prop="errors" @gui-input-error=" errors = $event.detail.message ? [$event.detail.message] : [] " ></gui-file-upload></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiFileUploadEvents, UploadService,} from '@golemui/gui-components';import '@golemui/gui-components/file-upload';import '@golemui/gui-components/index.css';
type FileUploadEvent<K extends keyof typeof GuiFileUploadEvents> = GuiEventMap< typeof GuiFileUploadEvents>[K];
// A stand-in for your upload endpoint, which resolves after a second.const uploadService: UploadService = { upload: (file) => new Promise((resolve) => setTimeout(() => resolve({ url: `/avatars/${file.name}` }), 1000), ),};
const dependencies = { uploadService };
@customElement('file-upload-limits')export class FileUploadLimits extends LitElement { @state() private errors: string[] = [];
protected override createRenderRoot() { return this; }
// The element reports a refused file or a failed upload: show it as an error. private onInputError(event: FileUploadEvent<'gui-input-error'>) { this.errors = event.detail.message ? [event.detail.message] : []; }
override render() { return html` <gui-file-upload id="avatar" label="Profile photo" hint="A PNG or JPEG, up to 1 MB." accept="image/png,image/jpeg" max-size="1048576" accept-message="Pick a PNG or JPEG image." max-size-message="Pick an image of 1 MB or less." .dependencies=${dependencies} .errors=${this.errors} @gui-input-error=${this.onInputError} ></gui-file-upload> `; }}<gui-file-upload id="avatar" label="Profile photo" hint="A PNG or JPEG, up to 1 MB." accept="image/png,image/jpeg" max-size="1048576" accept-message="Pick a PNG or JPEG image." max-size-message="Pick an image of 1 MB or less."></gui-file-upload>
<script type="module"> const avatar = document.querySelector('#avatar');
// A stand-in for your upload endpoint, which resolves after a second. avatar.dependencies = { uploadService: { upload: (file) => new Promise((resolve) => setTimeout(() => resolve({ url: `/avatars/${file.name}` }), 1000)), }, };
// The element reports a refused file or a failed upload: show it as an error. avatar.addEventListener('gui-input-error', (event) => { avatar.errors = event.detail.message ? [event.detail.message] : []; });</script>Failed uploads
Section titled “Failed uploads”When upload rejects, the file stays in the field, marked as failed, with a button to retry it
and one to remove it. The upload service below fails every other time: pick a file, then retry
it.
import { useState } from 'react';import { GuiFileUpload } from '@golemui/gui-components/react';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
let attempts = 0;
// A stand-in for an upload endpoint that fails the first time and works on the retry.const uploadService: UploadService = { upload: (file) => new Promise((resolve, reject) => setTimeout(() => { attempts += 1; if (attempts % 2 === 1) reject(new Error('The server is busy. Retry the upload.')); else resolve({ url: `/contracts/${file.name}` }); }, 1000), ),};const dependencies = { uploadService };
export function FileUploadRetry() { const [errors, setErrors] = useState<string[]>([]);
return ( <GuiFileUpload id="contract" label="Signed contract" 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, GuiFileUploadEvents, UploadService,} from '@golemui/gui-components';import '@golemui/gui-components/file-upload';
type FileUploadEvent<K extends keyof typeof GuiFileUploadEvents> = GuiEventMap< typeof GuiFileUploadEvents>[K];
@Component({ selector: 'app-file-upload-retry', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-file-upload id="contract" label="Signed contract" [dependencies]="dependencies" [errors]="errors()" (gui-input-error)="onInputError($event)" ></gui-file-upload> `,})export class FileUploadRetryComponent { errors = signal<string[]>([]); private attempts = 0;
// A stand-in for an upload endpoint that fails the first time and works on the retry. private uploadService: UploadService = { upload: (file) => new Promise((resolve, reject) => setTimeout(() => { this.attempts += 1; if (this.attempts % 2 === 1) reject(new Error('The server is busy. Retry the upload.')); else resolve({ url: `/contracts/${file.name}` }); }, 1000), ), };
dependencies = { uploadService: this.uploadService };
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/file-upload';import '@golemui/gui-components/index.css';
let attempts = 0;
// A stand-in for an upload endpoint that fails the first time and works on the retry.const uploadService: UploadService = { upload: (file) => new Promise((resolve, reject) => setTimeout(() => { attempts += 1; if (attempts % 2 === 1) reject(new Error('The server is busy. Retry the upload.')); else resolve({ url: `/contracts/${file.name}` }); }, 1000), ),};const dependencies = { uploadService };
const errors = ref<string[]>([]);</script>
<template> <gui-file-upload id="contract" label="Signed contract" :dependencies.prop="dependencies" :errors.prop="errors" @gui-input-error=" errors = $event.detail.message ? [$event.detail.message] : [] " ></gui-file-upload></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiFileUploadEvents, UploadService,} from '@golemui/gui-components';import '@golemui/gui-components/file-upload';import '@golemui/gui-components/index.css';
type FileUploadEvent<K extends keyof typeof GuiFileUploadEvents> = GuiEventMap< typeof GuiFileUploadEvents>[K];
@customElement('file-upload-retry')export class FileUploadRetry extends LitElement { @state() private errors: string[] = []; private attempts = 0;
// A stand-in for an upload endpoint that fails the first time and works on // the retry. private uploadService: UploadService = { upload: (file) => new Promise((resolve, reject) => setTimeout(() => { this.attempts += 1; if (this.attempts % 2 === 1) reject(new Error('The server is busy. Retry the upload.')); else resolve({ url: `/contracts/${file.name}` }); }, 1000), ), };
private dependencies = { uploadService: this.uploadService };
protected override createRenderRoot() { return this; }
private onInputError(event: FileUploadEvent<'gui-input-error'>) { this.errors = event.detail.message ? [event.detail.message] : []; }
override render() { return html` <gui-file-upload id="contract" label="Signed contract" .dependencies=${this.dependencies} .errors=${this.errors} @gui-input-error=${this.onInputError} ></gui-file-upload> `; }}<gui-file-upload id="contract" label="Signed contract"></gui-file-upload>
<script type="module"> const contract = document.querySelector('#contract'); let attempts = 0;
// A stand-in for an upload endpoint that fails the first time and works on the retry. contract.dependencies = { uploadService: { upload: (file) => new Promise((resolve, reject) => setTimeout(() => { attempts += 1; if (attempts % 2 === 1) reject(new Error('The server is busy. Retry the upload.')); else resolve({ url: `/contracts/${file.name}` }); }, 1000), ), }, };
contract.addEventListener('gui-input-error', (event) => { contract.errors = event.detail.message ? [event.detail.message] : []; });</script>An existing file
Section titled “An existing file”Set value to a file uploaded earlier, such as one your server returned with the record, and the
field shows it with its remove button. The upload button comes back once the user removes the
file; a file dropped on the field replaces it at once. A readonly field shows its file with no
buttons.
import { GuiFileUpload } 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: `/uploads/${file.name}` }), 1000), ),};const dependencies = { uploadService };
// Files uploaded earlier, as your server returned them.const coverLetter: FileItem = { id: 'f_cover-letter', name: 'cover-letter.pdf', size: 48213, type: 'application/pdf', status: 'uploaded', data: { url: '/uploads/cover-letter.pdf' },};
const idDocument: FileItem = { id: 'f_passport', name: 'passport.jpg', size: 210944, type: 'image/jpeg', status: 'uploaded', data: { url: '/uploads/passport.jpg' },};
export function FileUploadExisting() { return ( <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiFileUpload id="cover-letter" label="Cover letter" dependencies={dependencies} value={coverLetter} /> </div> <div className="gui-grid__cell"> <GuiFileUpload id="id-document" label="ID document" readOnly dependencies={dependencies} value={idDocument} /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/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: `/uploads/${file.name}` }), 1000), ),};
@Component({ selector: 'app-file-upload-existing', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-file-upload id="cover-letter" label="Cover letter" [dependencies]="dependencies" [value]="coverLetter" ></gui-file-upload> </div> <div class="gui-grid__cell"> <gui-file-upload id="id-document" label="ID document" readonly [dependencies]="dependencies" [value]="idDocument" ></gui-file-upload> </div> </div> `,})export class FileUploadExistingComponent { dependencies = { uploadService };
// Files uploaded earlier, as your server returned them. coverLetter: FileItem = { id: 'f_cover-letter', name: 'cover-letter.pdf', size: 48213, type: 'application/pdf', status: 'uploaded', data: { url: '/uploads/cover-letter.pdf' }, };
idDocument: FileItem = { id: 'f_passport', name: 'passport.jpg', size: 210944, type: 'image/jpeg', status: 'uploaded', data: { url: '/uploads/passport.jpg' }, };}<script setup lang="ts">import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/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: `/uploads/${file.name}` }), 1000), ),};const dependencies = { uploadService };
// Files uploaded earlier, as your server returned them.const coverLetter: FileItem = { id: 'f_cover-letter', name: 'cover-letter.pdf', size: 48213, type: 'application/pdf', status: 'uploaded', data: { url: '/uploads/cover-letter.pdf' },};
const idDocument: FileItem = { id: 'f_passport', name: 'passport.jpg', size: 210944, type: 'image/jpeg', status: 'uploaded', data: { url: '/uploads/passport.jpg' },};</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-file-upload id="cover-letter" label="Cover letter" :dependencies.prop="dependencies" :value.prop="coverLetter" ></gui-file-upload> </div> <div class="gui-grid__cell"> <gui-file-upload id="id-document" label="ID document" :readOnly="true" :dependencies.prop="dependencies" :value.prop="idDocument" ></gui-file-upload> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { FileItem, UploadService } from '@golemui/gui-components';import '@golemui/gui-components/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: `/uploads/${file.name}` }), 1000), ),};
const dependencies = { uploadService };
// Files uploaded earlier, as your server returned them.const coverLetter: FileItem = { id: 'f_cover-letter', name: 'cover-letter.pdf', size: 48213, type: 'application/pdf', status: 'uploaded', data: { url: '/uploads/cover-letter.pdf' },};
const idDocument: FileItem = { id: 'f_passport', name: 'passport.jpg', size: 210944, type: 'image/jpeg', status: 'uploaded', data: { url: '/uploads/passport.jpg' },};
@customElement('file-upload-existing')export class FileUploadExisting extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-file-upload id="cover-letter" label="Cover letter" .dependencies=${dependencies} .value=${coverLetter} ></gui-file-upload> </div> <div class="gui-grid__cell"> <gui-file-upload id="id-document" label="ID document" readonly .dependencies=${dependencies} .value=${idDocument} ></gui-file-upload> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-file-upload id="cover-letter" label="Cover letter"></gui-file-upload> </div> <div class="gui-grid__cell"> <gui-file-upload id="id-document" label="ID document" readonly></gui-file-upload> </div></div>
<script type="module"> // A stand-in for your upload endpoint, which resolves after a second. const uploadService = { upload: (file) => new Promise((resolve) => setTimeout(() => resolve({ url: `/uploads/${file.name}` }), 1000)), };
// Files uploaded earlier, as your server returned them. const coverLetter = document.querySelector('#cover-letter'); coverLetter.dependencies = { uploadService }; coverLetter.value = { id: 'f_cover-letter', name: 'cover-letter.pdf', size: 48213, type: 'application/pdf', status: 'uploaded', data: { url: '/uploads/cover-letter.pdf' }, };
const idDocument = document.querySelector('#id-document'); idDocument.dependencies = { uploadService }; idDocument.value = { id: 'f_passport', name: 'passport.jpg', size: 210944, type: 'image/jpeg', status: 'uploaded', data: { url: '/uploads/passport.jpg' }, };</script>A file you set with the uploading status can’t resume: its File is gone. The field shows it
as failed, with interrupted-message, until the user removes it.
In a form
Section titled “In a form”With a name, <gui-file-upload> submits its value as JSON under that name: the description of
the file and your server’s data, not the file. required blocks the submit while there is no
file.
A file that is still uploading, or that failed, blocks the submit too: it has no data to send
yet. Submit before the upload finishes to see it. pending-message and upload-error-message set
the error; {name} is the file name in the second.
import { useState, type SubmitEvent } from 'react';import { GuiButton, GuiFileUpload } 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: `/cv/${file.name}` }), 1000), ),};const dependencies = { uploadService };
export function FileUploadNativeForm() { const [result, setResult] = useState('');
// The browser runs this only once the CV has uploaded. const onSubmit = (event: SubmitEvent<HTMLFormElement>) => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); setResult(`Submitted ${JSON.stringify(data)}`); };
return ( <form id="application" onSubmit={onSubmit}> <GuiFileUpload id="application-cv" label="CV" name="cv" required dependencies={dependencies} /> <GuiButton type="submit" label="Apply" /> <p id="application-result" role="status"> {result} </p> </form> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/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: `/cv/${file.name}` }), 1000), ),};
@Component({ selector: 'app-file-upload-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="application" #form (submit)="onSubmit($event, form)"> <gui-file-upload id="application-cv" label="CV" name="cv" required [dependencies]="dependencies" ></gui-file-upload> <gui-button type="submit" label="Apply"></gui-button> <p id="application-result" role="status">{{ result() }}</p> </form> `,})export class FileUploadNativeFormComponent { dependencies = { uploadService }; result = signal('');
// The browser runs this only once the CV has uploaded. onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); this.result.set(`Submitted ${JSON.stringify(data)}`); }}<script setup lang="ts">import { ref } from 'vue';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/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: `/cv/${file.name}` }), 1000), ),};const dependencies = { uploadService };
const result = ref('');
// The browser runs this only once the CV has uploaded.function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; const data = Object.fromEntries(new FormData(event.currentTarget)); result.value = `Submitted ${JSON.stringify(data)}`;}</script>
<template> <form id="application" @submit.prevent="onSubmit"> <gui-file-upload id="application-cv" label="CV" name="cv" :required="true" :dependencies.prop="dependencies" ></gui-file-upload> <gui-button type="submit" label="Apply"></gui-button> <p id="application-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import type { UploadService } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/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: `/cv/${file.name}` }), 1000), ),};
const dependencies = { uploadService };
@customElement('file-upload-native-form')export class FileUploadNativeForm extends LitElement { @query('#application') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
// The browser runs this only once the CV has uploaded. private onSubmit(event: SubmitEvent) { event.preventDefault(); const data = Object.fromEntries(new FormData(this.form)); this.result = `Submitted ${JSON.stringify(data)}`; }
override render() { return html` <form id="application" @submit=${this.onSubmit}> <gui-file-upload id="application-cv" label="CV" name="cv" required .dependencies=${dependencies} ></gui-file-upload> <gui-button type="submit" label="Apply"></gui-button> <p id="application-result" role="status">${this.result}</p> </form> `; }}<form id="application"> <gui-file-upload id="application-cv" label="CV" name="cv" required></gui-file-upload> <gui-button type="submit" label="Apply"></gui-button> <p id="application-result" role="status"></p></form>
<script type="module"> // A stand-in for your upload endpoint, which resolves after a second. document.querySelector('#application-cv').dependencies = { uploadService: { upload: (file) => new Promise((resolve) => setTimeout(() => resolve({ url: `/cv/${file.name}` }), 1000)), }, };
const form = document.querySelector('#application'); const result = document.querySelector('#application-result');
// The browser runs this only once the CV has uploaded. form.addEventListener('submit', (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); result.textContent = `Submitted ${JSON.stringify(data)}`; });</script>How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<gui-file-upload> renders into the light DOM, so you can style every part with plain CSS. These
are the parts and their public classes, with a file uploading:
<gui-file-upload class="gui-field"> <span class="gui-label" id="resume_label"> <span class="gui-label__text">Résumé</span> <div class="gui-widget-hint" id="resume_hint" aria-hidden="true"> A PDF works best. </div> </span> <div class="gui-widget"> <div class="gui-widget-input gui-file-upload__box" role="group" aria-labelledby="resume_label" aria-describedby="resume_hint" > <div class="gui-file-upload__bar gui-file-upload__bar--uploading" data-status="uploading" style="--gui-upload-pct: 40%" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="40" aria-valuetext="resume.pdf, 40%" > <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">resume.pdf</span> <span class="gui-file-upload__pct">40%</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 resume.pdf" > <svg aria-hidden="true">…</svg> </button> </div> <input type="file" id="resume" class="gui-visually-hidden gui-file-upload__input" tabindex="-1" aria-hidden="true" /> </div> </div> <div class="gui-visually-hidden" role="status" aria-live="polite"></div> <div class="gui-validator" id="resume_errors" role="alert"></div></gui-file-upload>With no file, the upload button takes the place of the bar:
<button type="button" class="gui-file-upload__button"> <svg aria-hidden="true">…</svg> <span>Upload file</span></button>| Class | Part |
|---|---|
gui-field | The element itself. Lines its parts up with the fields next to it in a gui-grid row. |
gui-label, gui-label__text | The label and its text. |
gui-widget-hint | The hint. |
gui-widget | The box around the control. |
gui-widget-input, gui-file-upload__box | The field: the drop target, with the bar or the button. |
gui-file-upload__box--dragover | On the field while a file is dragged over it. |
gui-file-upload__box--disabled | On the field while disabled, or without an upload service. |
gui-file-upload__box--readonly | On the field while read-only. |
gui-file-upload__box--icon | On the field when there is an icon: it makes room for it. |
gui-widget-icon | The icon, with your icon classes. |
gui-file-upload__button | The upload button. |
gui-file-upload__bar | The file: its name, progress and buttons. data-status holds its status. |
gui-file-upload__bar--uploading | On the bar while the file uploads. |
gui-file-upload__bar--error | On the bar when the file failed. |
gui-file-upload__bar--removing | On the bar while remove runs. |
gui-file-upload__fill | The progress fill, as wide as --gui-upload-pct. |
gui-file-upload__text | The file name and the percentage. |
gui-file-upload__text-overlay | A copy of the text in the fill’s colors, clipped to the fill. |
gui-file-upload__name | The file name. |
gui-file-upload__pct | The percentage. |
gui-file-upload__action | The cancel and remove button. |
gui-file-upload__action--retry | The retry button. |
gui-file-upload__action--busy | On the remove button while remove runs, with a spinner. |
gui-file-upload__service-error | The message shown without an upload service. |
gui-file-upload__input | The native file input, hidden. The upload button opens it. |
gui-validator, gui-validator__error | The errors container and one error message. |
The ids derive from uid, or from a generated id such as gui-file-upload-2 when you don’t set
one. Select by class or by tag, not by id.
Tokens
Section titled “Tokens”The field reads these design tokens. Set them on :root to change every field, or on any element
to change the fields inside it:
| Token | What it styles |
|---|---|
--gui-widget-size | The height of the field, and of the upload button. |
--gui-radius-md | The corner radius of the field. |
--gui-bg-surface | The field’s background, and the buttons’ on the bar. |
--gui-border-default | The border, and the dashed border of the upload button. |
--gui-text-default | The text and the buttons. |
--gui-text-secondary | The hint. |
--gui-font-family | The font. |
--gui-font-sm | The text size of the label and the upload button. |
--gui-font-xs | The text size of the percentage, the hint and the errors. |
--gui-intent-primary | The progress fill. |
--gui-intent-primary-text | The file name over the fill. |
--gui-transition-normal | The speed of the fill. |
--gui-border-focus | The border while focused or while a file is dragged over it. |
--gui-shadow-focus | The focus ring. |
--gui-intent-error | The border when there are errors, and the remove button on hover. |
--gui-shadow-focus-error | The focus ring when there are errors. |
--gui-intent-error-text | The message shown without an upload service. |
--gui-bg-disabled | The background when disabled. |
--gui-text-disabled | The text when disabled. |
--gui-bg-default | The upload button’s background on hover. |
--gui-space-1 … --gui-space-3 | The space between the parts. |
/* A brand progress fill in the uploads section. */.uploads { --gui-intent-primary: #2f7d6d; --gui-intent-primary-text: #ffffff;}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 upload button and the file. Without a label the field has no accessible name, and development builds log a warning. - The upload button. The native file input is hidden from assistive technology: the upload
button opens it.
button-labelchanges its text, “Upload file” by default. - Progress. While a file uploads, the bar is a progress bar, with the file name and the percentage as its value.
- Announcements. Screen readers hear when a file uploads, fails or is removed, such as
“resume.pdf uploaded.”
uploaded-message,failed-messageandremoved-messagechange them;{name}is the file name. - Buttons. The cancel, retry and remove buttons are named after the file, such as “Remove
resume.pdf”.
cancel-aria-label,retry-aria-labelandremove-aria-labelchange them, with{name}for the file name. - Focus. After the user removes or cancels a file, focus moves to the upload button. After a retry, it moves to the cancel button.
- Errors. While errors show, the field has
aria-invalid="true"and points at them witharia-errormessage. - Drag and drop needs a mouse or touch: the upload button does the same with the keyboard.
| Key | Action |
|---|---|
| Tab | Moves focus to the upload button, then to the file’s buttons. |
| 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 uploadFile, fileUploaded,
fileFailed, fileRemoved, removeFile, cancelFile, retryFile, fileTooLarge,
fileTypeNotAccepted, uploadFailed, uploadInterrupted, removeFailed and
missingUploadService keys. See i18n. What every element shares
is in Accessibility.