FileUpload
A styled file input / dropzone for selecting a file to upload.
Last updated
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.
Show codeHide code
<FileUpload name="file" accept="image/*" />Add
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
---
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 —
disabledon 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:
Show codeHide code
<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:
Show codeHide code
<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:
Show codeHide code
<FileUpload name="doc" accept=".pdf" />Marked required:
<FileUpload name="resume" accept=".pdf,.doc,.docx" required />
Disabled:
Show codeHide code
<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.
Show codeHide code
<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.
brief.pdf · 240 KB
64%photo.heic · 4.2 MB
Upload failed.
Show codeHide code
<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
UploadCloudicon isaria-hidden. - The prompt (the default slot) names the input; the chosen file name and the
hintdescribe it, so they do not pile into the name. Passaria-labelto name it yourself. - Without
multiple, dropping several files is refused with a message. - A consumer-passed
idis 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 inField; add a visible label of your own above it if you need one.hintis wired witharia-describedbyand 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
JsNoteabove.
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
acceptandmaxSize; they are checked on the client for drops and picks, and still need a server-side check. - Do use
hintfor the accepted types and size; add a heading above it when it needs a label, since it doesn’t wire intoField. - 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.”