Skip to content

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.

1 file selected
  • prices-2026-09.csv184 kB

CSV or Parquet, up to 10 MB each.

1 file selected · 184 kB total

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

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 kB
    Waiting
  • funding.csv12.3 kB
    Uploading
  • universe.parquet2.4 MB
    Uploaded
  • signals.csv90.1 kB
    The server rejected this file (413)

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.

3 files selected
  • prices.csv400 B
  • notes.txt300 B
  • huge.csv4.0 kB

States

Empty, filled, disabled and the drop target — the same component, four states a reviewer has to see.

CSV up to 10 MB

Empty

2 files selected
  • prices-2026-09.csv184 kB
  • funding.csv12.3 kB

Selected

Uploads are closed for this workspace.

Disabled

Drop files here

Drop or browse

Drop zone

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.

Exported symbols
ExportKindSignature
FileUpload15 documented props({ items, onFiles, onReject, onRemove, onRetry, accept, multiple, maxSize, maxFiles, title, inputLabel, hint, disabled, listLabel, footer, className, ...props }: FileUploadProps) => import("@types/re…
Exported symbols
ExportKindSignature
DropZone15 documented props(props: Omit<FileInputProps, "variant">) => import("@types/react").JSX.Element
FileInput16 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.

FileUpload props
PropTypeDefaultNotes
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…
itemsreadonly 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.

DropZone props
PropTypeDefaultNotes
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.

FileInput props
PropTypeDefaultNotes
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.

Luc UI v1.0FileInputdark theme