# File Upload

> A field for uploading files. A CSS component with two classes and no JavaScript.

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

**Plain HTML**

```html
<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 -->
```

```html
<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>
```

**TypeScript**

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

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

```ts
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](/type-safe-use.md).

| 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

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

**Plain HTML**

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

```html
<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>
```

```js
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`.

## 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](/customising.md) for how the variables and the layer work.

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