Components · FileInput and FileUpload
FileInput and FileUpload
Two vocabularies and one transfer surface: a compact row for ordinary attachments, a larger drop target when dragging is the point, and FileUpload when the rows themselves have to show progress. None of them uploads anything — they hand you File objects and stop.
import { DropZone, Field, FileInput, FileUpload } from "@luc/ui";Examples
Compact file field
A field row, not a giant dashed rectangle. Drop a file on either variant — the accept list and the size limit are enforced in both.
- prices-2026-09.csv184 kB
CSV or Parquet, up to 10 MB each.
1 file selected · 184 kB total
<Field label="Attachments" description="CSV or Parquet, up to 10 MB each.">
<FileInput
accept={[".csv", ".parquet", "text/csv"]}
multiple
maxSize={10_000_000}
value={files}
onFilesChange={setFiles}
onReject={(items) => setRejected(items.length)}
hint="CSV up to 10 MB"
/>
</Field>Drop zone
Use it when dropping is the primary gesture — an import step, a bulk upload. It is still a file *selection* surface: no progress bar, no retry, no network.
Drop files here
PNG, JPEG or CSV · up to 4 MB
<DropZone
accept={["image/*", ".csv"]}
multiple
maxSize={4_000_000}
hint="PNG, JPEG or CSV · up to 4 MB"
/>Transfer rows
`FileUpload` renders the transfer rather than performing it: queue, progress, success, failure with a retry, and the remove control — all driven by the `items` you own.
Drop files here
Nothing here leaves the browser: the rows are fixtures, and the transfer is the caller's.
- prices-2026-09.csv184 kBWaiting
- funding.csv12.3 kBUploading
- universe.parquet2.4 MBUploaded
- signals.csv90.1 kBThe server rejected this file (413)
<FileUpload
items={items}
onFiles={startUpload}
onRemove={removeItem}
onRetry={retryItem}
accept=".csv"
multiple
hint="CSV or Parquet, up to 10 MB each"
listLabel="Uploaded runs"
/>Rejections are visible
A rejected file gets a name and a reason instead of disappearing. The field also reports it as invalid, so a form can block submission.
- prices.csv400 B
- notes.txt300 B
- huge.csv4.0 kB
2 files were rejected
<Field label="Strict import" error="2 files were rejected">
<FileInput accept={[".csv"]} multiple maxSize={1_000} defaultValue={files} />
</Field>States
Empty, filled, disabled and the drop target — the same component, four states a reviewer has to see.
Empty
- prices-2026-09.csv184 kB
- funding.csv12.3 kB
Selected
Disabled
Drop files here
Drop or browse
Drop zone
<FileInput accept={[".csv"]} hint="CSV up to 10 MB" />
<FileInput multiple accept={[".csv"]} defaultValue={files} />
<FileInput disabled accept={[".csv"]} hint="Uploads are closed for this workspace." />
<DropZone accept={[".csv"]} hint="Drop or browse" />Usage
import { DropZone, FileInput, FileUpload } from "@luc/ui";<FileInput accept={[".csv", "text/csv"]} multiple onFilesChange={setFiles} aria-label="Upload runs" />The same component with `variant="dropzone"` — exported as `DropZone` — is the larger target for when dragging is the primary gesture.
<FileUpload items={items} onFiles={startUpload} onRemove={removeItem} accept=".csv" multiple />`items` is your own state, including each row's `status` and `progress`: the component renders the transfer and never performs one.
The components select, validate and render the transfer; the application uploads. Nothing here sends a byte anywhere.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
This page documents two sibling modules: `FileInput` with its `DropZone` variant, which selects and validates files, and `FileUpload` beside it, which renders a transfer you own.
| Export | Kind | Signature |
|---|---|---|
FileUpload | 15 documented props | ({ items, onFiles, onReject, onRemove, onRetry, accept, multiple, maxSize, maxFiles, title, inputLabel, hint, disabled, listLabel, footer, className, ...props }: FileUploadProps) => import("@types/re… |
| Export | Kind | Signature |
|---|---|---|
DropZone | 15 documented props | (props: Omit<FileInputProps, "variant">) => import("@types/react").JSX.Element |
FileInput | 16 documented props | ({ value, defaultValue, onFilesChange, onReject, accept, multiple, maxSize, maxFiles, disabled, invalid, variant, hint, label, id, className, ...props }: FileInputProps) => import("@types/react").JSX… |
FileUpload props
Type: FileUploadProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
accept? | string | undefined | — | Same syntax as the native input, e.g. `image/*,.csv`. |
disabled? | boolean | undefined | — | |
footer? | ReactNode | — | Rendered between the drop target and the list. |
hint? | string | undefined | — | |
inputLabel? | string | undefined | — | Accessible name for the hidden `<input type="file">`. The input is `sr-only` and opened by the visible "Browse files" button, so nothing in the accessibility tree named it: an unl… |
items | readonly FileUploadItem[] | — | |
listLabel? | string | undefined | — | Accessible name for the list of files. |
maxFiles? | number | undefined | — | Ceiling on how many files a single drop may add. |
maxSize? | number | undefined | — | Per-file ceiling in bytes. |
multiple? | boolean | undefined | — | |
onFiles | (files: File[]) => void | — | Validated selection or drop. The caller starts the transfer. |
onReject? | ((rejections: FileUploadRejection[]) => void) | undefined | — | |
onRemove? | ((id: string) => void) | undefined | — | |
onRetry? | ((id: string) => void) | undefined | — | |
title? | string | undefined | — |
DropZone props15 props
DropZone props
Type: Omit<FileInputProps, "variant">
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
accept? | string[] | undefined | — | e.g. `["image/*", ".csv"]`. Passed to the picker and checked on drop. |
aria-label? | string | undefined | — | |
className? | string | undefined | — | |
defaultValue? | File[] | undefined | — | |
disabled? | boolean | undefined | — | |
hint? | ReactNode | — | Constraint line, e.g. "CSV up to 10 MB". |
id? | string | undefined | — | |
invalid? | boolean | undefined | — | |
label? | string | undefined | — | |
maxFiles? | number | undefined | — | |
maxSize? | number | undefined | — | Bytes. Larger files are rejected with a message, not silently dropped. |
multiple? | boolean | undefined | — | |
onFilesChange? | ((files: File[]) => void) | undefined | — | |
onReject? | ((rejections: FileRejection[]) => void) | undefined | — | |
value? | File[] | undefined | — |
FileInput props16 props
FileInput props
Type: FileInputProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
accept? | string[] | undefined | — | e.g. `["image/*", ".csv"]`. Passed to the picker and checked on drop. |
aria-label? | string | undefined | — | |
className? | string | undefined | — | |
defaultValue? | File[] | undefined | — | |
disabled? | boolean | undefined | — | |
hint? | ReactNode | — | Constraint line, e.g. "CSV up to 10 MB". |
id? | string | undefined | — | |
invalid? | boolean | undefined | — | |
label? | string | undefined | — | |
maxFiles? | number | undefined | — | |
maxSize? | number | undefined | — | Bytes. Larger files are rejected with a message, not silently dropped. |
multiple? | boolean | undefined | — | |
onFilesChange? | ((files: File[]) => void) | undefined | — | |
onReject? | ((rejections: FileRejection[]) => void) | undefined | — | |
value? | File[] | undefined | — | |
variant? | "compact" | "dropzone" | undefined | — | `compact` is a single row; `dropzone` is the larger drag area vocabulary. |
Guidelines
Do
- Pass `accept` and `maxSize` together: the picker filters what it can and the drop path validates what it cannot.
- Read `onReject` and show the reason, because a file that disappears without a sentence reads as a bug.
- Keep the compact variant in a form row, and reach for `DropZone` when importing is the whole task.
- Drive `items` from your own upload code, so a failure keeps its message, its retry and its place in the list.
- Set `status` honestly — queued, uploading, done, error — because the row's treatment and announcement follow it.
- Give the list a `listLabel` when it sits away from the target, so it is a named region a reader can navigate to.
Don't
- Imply a transfer that happened: this component selects and validates files, and the upload is the caller's.
- Leave `maxSize` off a surface that can receive a multi-gigabyte file; the ceiling is documentation the reader needs.
- Use a drop zone as a decorative panel — a dashed target is a promise that dropping does something.
- Render a progress bar you cannot fill: omit `progress` while the transfer length is unknown instead of parking it at zero.
- Retry in the background on failure — expose `onRetry` and let the reader decide when to try again.
- Reject a file without saying why: the rejection carries a `reason` and a sentence, and both belong on screen.
Accessibility
The picker is a real file input behind a labelled row, so it is reachable by keyboard and announced by name; the drop area is an extra path rather than the only one. A rejected file is reported as text with its reason and sets the field invalid, and `hint` is wired to the input instead of merely printed beside it.
The drop target carries a real Browse files button and the file list is a labelled list, so the whole surface works without a pointer. Every row states its file's name, size and status as text — including the failure reason — and the retry and remove controls are named buttons rather than bare glyphs.