# FileUpload

> A styled file input / dropzone for selecting a file to upload.

URL: https://apogee.avaui.dev/docs/file-upload
Last updated: 2026-10-15

A styled file input / dropzone for selecting a file to upload.

Last updated October 15, 2026

Uses JavaScript

Shows the selected file name(s) after a pick, and adds drag-and-drop: dropping a file onto the dropzone sets it on the input and displays its name.

**Without JS:** The native picker still works — clicking or tabbing to the label opens it, and the field submits with the form. The selected-file-name text stays hidden (\`no-js:hidden\`), and drag-and-drop isn't available.

```astro
<FileUpload name="file" accept="image/*" />
```

## Add

```sh
npx @ava-ui/apogee add file-upload
```

Bundles `@lucide/astro` for the `UploadCloud` glyph. No Apogee dependencies. Ships one scoped JS island (progressive enhancement).

## Usage

```astro
---
import FileUpload from '@/components/apogee/FileUpload/FileUpload.astro'
---
<FileUpload name="avatar" accept="image/png,image/jpeg" />
```

## API

Any other native `<input type="file">` attribute (e.g. `required`, `disabled`, `capture`) passes through to the input.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | — | Form field name, forwarded to the native input. |
| `accept` | `string` | — | Accepted file types, forwarded to the native input (e.g. `"image/*"`). |
| `multiple` | `boolean` | `false` | Allow selecting more than one file, forwarded to the native input. |
| `maxSize` | `number` | — | Largest file allowed, in bytes. Larger files are refused (dropped or picked) and the hint says why. |
| `hint` | `string` | — | A line under the control; it turns critical and shows the reason when a file is refused. |
| `preview` | `boolean` | `false` | Shows a thumbnail of a picked image (JS). |
| `variant` | `'default' \| 'headless'` | `'default'` | `headless` drops the dashed box so the slot is the whole look. |
| `inline` | `boolean` | `false` | A single row (icon + text) instead of a stacked zone. |
| `messages` | `{ type?, size?, one? }` | English | Refusal messages: wrong type, too large, several files dropped on a single-file input. `{name}` is the file, `{max}` the limit. |
| `class` | `string` | — | Extra classes on the label (dropzone), merged last. |

## States

-   **Hover** — dropzone border shifts to `--border-primary`.
-   **Drag-over** (JS) — border and a faint accent background via `data-drag="true"`.
-   **Invalid** (JS) — a refused file sets `data-invalid="true"`: critical border, the hint shows the reason. Clears on the next accepted file.
-   **Disabled** — `disabled` on the input dims the zone and blocks the pointer.
-   **No JS** — filename text is hidden (`no-js:hidden`); the native picker and form submission still work.

## Examples

Multiple files:

```astro
<FileUpload name="attachments" multiple />
```

Restricted to a type and size, with a hint and an image preview. Drop a text file or a large image to see the refusal:

```astro
<FileUpload name="photo" accept="image/*" maxSize={1024 * 1024} hint="PNG or JPG, up to 1 MB" preview />
```

Without a hint, a refused file still says why:

```astro
<FileUpload name="doc" accept=".pdf" />
```

Marked required:

```astro
<FileUpload name="resume" accept=".pdf,.doc,.docx" required />
```

Disabled:

```astro
<FileUpload name="locked" disabled hint="Uploads are closed" />
```

## Slot, headless and inline

The default slot replaces the built-in copy; `headless` leaves styling to you; `inline` makes a single row.

```astro
<FileUpload name="cv"><span>Drop your CV here</span></FileUpload>
<FileUpload name="avatar" variant="headless"><Button as="span" variant="outline" color="neutral">Choose a photo</Button></FileUpload>
<FileUpload name="attachment" inline />
```

## Events

A dropped file that passes `accept` and `maxSize` assigns `input.files` and dispatches a native `change` event, the same as picking a file. A refused file leaves `input.files` empty; a picked file that fails the same checks is cleared. Native `accept` only filters the picker dialog, so the island applies the same rule to drops.

## Upload list

Rows under the zone while files upload: a Progress with its value at the end, a CloseButton to cancel, and a failed row with a retry. `readableSize` from `utils/files.ts` formats the size.

```astro
<ul class="flex flex-col gap-2">
<li class="flex items-center gap-3 rounded-lg border border-neutral p-3">
  <FileText class="size-5 text-neutral-faded" aria-hidden="true" />
  <div class="min-w-0 flex-1">
    <p class="truncate text-sm font-medium">brief.pdf <span class="font-normal text-neutral-faded">· 240 KB</span></p>
    <Progress value={64} labelPosition="end" label="Uploading brief.pdf" size="small" />
  </div>
  <CloseButton size="small" label="Cancel upload" />
</li>
</ul>
```

## Accessibility

| Key | Action |
| --- | --- |
| `Tab` | Moves focus to the file input. |
| `Enter` `Space` | Opens the system file picker. |

-   The visible control is a `<label>` wrapping a visually-hidden (`sr-only`) native `<input type="file">`.
-   The `UploadCloud` icon is `aria-hidden`.
-   The prompt (the default slot) names the input; the chosen file name and the `hint` describe it, so they do not pile into the name. Pass `aria-label` to name it yourself.
-   Without `multiple`, dropping several files is refused with a message.
-   A consumer-passed `id` is used for both the `<input>` and the `<label for>`, so the association stays correct; omit it and the component generates its own. It isn’t designed to be wrapped in `Field`; add a visible label of your own above it if you need one. `hint` is wired with `aria-describedby` and announced when it changes (`aria-live="polite"`).
-   **No JS:** the picker and form submission are unaffected — only the filename echo and drag-and-drop are lost. See the `JsNote` above.

## Tokens

| Token | Used for |
| --- | --- |
| `--border-neutral` / `--border-primary` / `--border-critical` | Dropzone border (default / hover and drag-over / invalid) |
| `--background-page` | Dropzone fill |
| `--background-primary-faded-highlighted` | Drag-over background tint |
| `--foreground-critical` | Hint while a file is refused |
| `--foreground-neutral-faded` | Helper text and filename echo |
| `--foreground-neutral` | “or drag a file here” text |
| `--radius-lg` | Corner radius |

## Do / Don’t

-   **Do** set `accept` and `maxSize`; they are checked on the client for drops and picks, and still need a server-side check.
-   **Do** use `hint` for the accepted types and size; add a heading above it when it needs a label, since it doesn’t wire into `Field`.
-   **Don’t** rely on drag-and-drop as the only way in — the label/click path (and no-JS path) must always work.
-   **Don’t** remove the visible “Browse” text — it’s the only affordance once drag-and-drop is unavailable.

## Content

-   Keep the static copy as-is (“Browse or drag a file here”) unless the accepted types need calling out — add that as separate text near the component, not by editing its copy.
-   Nearby helper text should state format and size limits, e.g. “PNG or JPG, up to 5 MB.”

## Related

[

InputA labeled text field with hint/error messaging, wired for accessibility — plus a pure-CSS floating label.

](https://apogee.avaui.dev/docs/input)[

FieldThe labeled-field scaffold — label, control, and hint/error — wired for accessibility.

](https://apogee.avaui.dev/docs/field)[

ProgressA determinate or indeterminate progress bar.

](https://apogee.avaui.dev/docs/progress)
