Skip to content

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.

AttributeEffect
multipleSeveral files can be chosen
acceptThe 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.

You write the list yourself, since only you know what should happen when a file is removed.

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.form
let 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.

ClassWhat it is
.lt-file-uploadThe field. The state comes from data-state.
.lt-file-upload-listThe list of chosen files
VariableDefault
--lt-file-upload-paddingvar(--lt-spacing-2)
--lt-file-upload-radiusvar(--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.

  • 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.