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 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 — 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:

Show 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 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 code
<FileUpload name="doc" accept=".pdf" />

Marked required:

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

Disabled:

Show 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 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 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 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.”