File Upload
The class goes on a plain <input type="file">. The button inside is the browser’s own, and its text comes from the operating system’s language. It cannot be translated, so the label above the field has to say what to upload.
How to use it
Section titled “How to use it”PDF or image, up to 10 MB per file.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/file-upload/file-upload.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/label/label.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/help-text/help-text.css"><!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css --><label class="lt-label" for="attachments">Upload attachments</label><input class="lt-file-upload" type="file" id="attachments" multiple accept=".pdf,.jpg" aria-describedby="attachments-help" /><p class="lt-help-text" id="attachments-help">PDF or image, up to 10 MB per file.</p>import "@lo-tek/interfaces/tokens.css"import "@lo-tek/interfaces/file-upload.css"import "@lo-tek/interfaces/label.css"import "@lo-tek/interfaces/help-text.css"lt is used in two ways, and which one depends on whether you create the element or find it.
If you create the markup, spread the attributes in. That is the way in React, Astro and anything else that renders:
import { lt } from "@lo-tek/interfaces/react"
<input {...lt.fileUpload()} id="attachments" accept=".pdf,.jpg" />The import is from /react here, since the block is JSX. From there the keys are className and htmlFor. In Astro and anything else without React, import @lo-tek/interfaces and you get class and for.
If the element is already there, set them on it. That is the way when you enhance HTML a server or a template has sent:
import { lt } from "@lo-tek/interfaces"
const field = document.querySelector<HTMLInputElement>("#attachments")
lt.setAttributes(field, lt.fileUpload({ multiple: true, accept: ".pdf" }))lt.fileUpload.list gives the class for the list. See Type-safe use.
| Attribute | Effect |
|---|---|
multiple | Several files can be chosen |
accept | The file types the file picker suggests |
data-state="invalid" | Red border. Set aria-invalid="true" as well. |
data-state="success" | Green border |
accept is a hint in the file picker, not a restriction. The user can choose “all files”, so the type has to be checked on the server anyway.
The list of chosen files
Section titled “The list of chosen files”You write the list yourself, since only you know what should happen when a file is removed.
- birth-certificate.pdf
- tenancy-agreement.pdf
<label class="lt-label" for="attachments-2">Upload attachments</label><input class="lt-file-upload" type="file" id="attachments-2" multiple />
<ul class="lt-file-upload-list"><li> birth-certificate.pdf <button class="lt-button" data-variant="ghost" type="button">Remove birth-certificate.pdf</button></li><li> tenancy-agreement.pdf <button class="lt-button" data-variant="ghost" type="button">Remove tenancy-agreement.pdf</button></li></ul>The button text should say which file is removed. “Remove” alone gives identical buttons to someone listening to the page.
The field only remembers the latest selection: if the user picks two more files, they replace the previous ones. If the files should accumulate, and be removable one at a time, you have to keep the list yourself, and send it yourself:
<form action="/application/attachments"> <label class="lt-label" for="attachments">Upload attachments</label> <input class="lt-file-upload" type="file" id="attachments" name="attachments" multiple /> <ul class="lt-file-upload-list"></ul> <button class="lt-button" type="submit">Send</button></form>const field = document.getElementById("attachments")const form = field.formlet files = []
field.addEventListener("change", () => { files = [...files, ...field.files] // Draw the list of files here, with a "Remove" button per file. field.value = ""})
form.addEventListener("submit", async (event) => { event.preventDefault() const data = new FormData(form) // The empty field would otherwise send an empty file under its own name. data.delete(field.name) for (const file of files) data.append(field.name, file) await fetch(form.action, { method: "POST", body: data })})The field has to be inside a <form> and have a name. It is cleared after every selection, so an ordinary submission includes none of the files. That is why the form is sent with fetch, with the files added to FormData.
Classes you can style
Section titled “Classes you can style”| Class | What it is |
|---|---|
.lt-file-upload | The field. The state comes from data-state. |
.lt-file-upload-list | The list of chosen files |
| Variable | Default |
|---|---|
--lt-file-upload-padding | var(--lt-spacing-2) |
--lt-file-upload-radius | var(--lt-spacing-1) |
The button inside the field is styled through ::file-selector-button, which is the only way in to it.
See Customising for how the variables and the layer work.
Accessibility
Section titled “Accessibility”- Say which file types and which maximum size are accepted before the user tries. An error message afterwards is a worse alternative.
- Announce when a file is added or removed, in an element with
role="status". - Drag and drop can come in addition, never instead. It does not exist for the keyboard.
- During upload: show progress per file, and let the user cancel.