Skip to content
Components, switch product

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.

Code of the example above
FileUploadBasic.tsx
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}
/>
);
}

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.

ResumeField.tsx
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)}
/>
);
}

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 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:

upload-service.js
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.');
},
};
main.js
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. file is the browser’s File. context has onProgress, to report a percentage from 0 to 100, signal, an AbortSignal that 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, and path, the element’s path.
  • 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 calls remove for 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 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:

FieldWhat it holds
idAn id the element gives the file when the user picks it.
nameThe file name.
sizeThe size, in bytes.
typeThe MIME type the browser reports, such as application/pdf.
statusuploading, uploaded or error.
errorWhy the file failed, while status is error.
dataWhat 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.

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.

Code of the example above
FileUploadLimits.tsx
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] : [])
}
/>
);
}

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.

Code of the example above
FileUploadRetry.tsx
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] : [])
}
/>
);
}

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.

Code of the example above
FileUploadExisting.tsx
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>
);
}

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.

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.

Code of the example above
FileUploadNativeForm.tsx
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>
);
}

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

<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
<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:

gui-file-upload, empty
<button type="button" class="gui-file-upload__button">
<svg aria-hidden="true">…</svg>
<span>Upload file</span>
</button>
ClassPart
gui-fieldThe element itself. Lines its parts up with the fields next to it in a gui-grid row.
gui-label, gui-label__textThe label and its text.
gui-widget-hintThe hint.
gui-widgetThe box around the control.
gui-widget-input, gui-file-upload__boxThe field: the drop target, with the bar or the button.
gui-file-upload__box--dragoverOn the field while a file is dragged over it.
gui-file-upload__box--disabledOn the field while disabled, or without an upload service.
gui-file-upload__box--readonlyOn the field while read-only.
gui-file-upload__box--iconOn the field when there is an icon: it makes room for it.
gui-widget-iconThe icon, with your icon classes.
gui-file-upload__buttonThe upload button.
gui-file-upload__barThe file: its name, progress and buttons. data-status holds its status.
gui-file-upload__bar--uploadingOn the bar while the file uploads.
gui-file-upload__bar--errorOn the bar when the file failed.
gui-file-upload__bar--removingOn the bar while remove runs.
gui-file-upload__fillThe progress fill, as wide as --gui-upload-pct.
gui-file-upload__textThe file name and the percentage.
gui-file-upload__text-overlayA copy of the text in the fill’s colors, clipped to the fill.
gui-file-upload__nameThe file name.
gui-file-upload__pctThe percentage.
gui-file-upload__actionThe cancel and remove button.
gui-file-upload__action--retryThe retry button.
gui-file-upload__action--busyOn the remove button while remove runs, with a spinner.
gui-file-upload__service-errorThe message shown without an upload service.
gui-file-upload__inputThe native file input, hidden. The upload button opens it.
gui-validator, gui-validator__errorThe 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.

The field reads these design tokens. Set them on :root to change every field, or on any element to change the fields inside it:

TokenWhat it styles
--gui-widget-sizeThe height of the field, and of the upload button.
--gui-radius-mdThe corner radius of the field.
--gui-bg-surfaceThe field’s background, and the buttons’ on the bar.
--gui-border-defaultThe border, and the dashed border of the upload button.
--gui-text-defaultThe text and the buttons.
--gui-text-secondaryThe hint.
--gui-font-familyThe font.
--gui-font-smThe text size of the label and the upload button.
--gui-font-xsThe text size of the percentage, the hint and the errors.
--gui-intent-primaryThe progress fill.
--gui-intent-primary-textThe file name over the fill.
--gui-transition-normalThe speed of the fill.
--gui-border-focusThe border while focused or while a file is dragged over it.
--gui-shadow-focusThe focus ring.
--gui-intent-errorThe border when there are errors, and the remove button on hover.
--gui-shadow-focus-errorThe focus ring when there are errors.
--gui-intent-error-textThe message shown without an upload service.
--gui-bg-disabledThe background when disabled.
--gui-text-disabledThe text when disabled.
--gui-bg-defaultThe upload button’s background on hover.
--gui-space-1 … --gui-space-3The space between the parts.
styles.css
/* 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.

  • Name. The label names 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-label changes 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-message and removed-message change 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-label and remove-aria-label change 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 with aria-errormessage.
  • Drag and drop needs a mouse or touch: the upload button does the same with the keyboard.
KeyAction
TabMoves focus to the upload button, then to the file’s buttons.
Enter or SpaceActivates 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.