Input File

A file picker shaped like an Input that also runs the upload. Hand it onUpload and it moves each file through queued, uploading, done or error, reports progress, and keeps failures on screen with a retry.

src/components/ui/input-file.tsx

About

From the component's own source, where it was written.

A file picker shaped like an Input, which also runs the upload.

The counterpart to Dropzone, not a replacement: this is the control you put on a settings form beside a Label, where a full drop card would be absurd. Dropzone is the better default for a standalone uploader — dropping is the primary gesture there, and it has room to say what is happening.

The native control is replaced rather than restyled. input[type=file] gives you a UA button whose text is not settable and whose layout is not reachable except through a vendor pseudo-element, so matching the kit's field metrics means driving a hidden input from our own trigger.

The upload lives in useUploads, shared with Dropzone, so the two shapes cannot drift in behaviour.

Install

npx astralyx-ui add input-file

Usage

import { InputFile } from '@/components/ui/input-file'

<InputFile
  multiple
  accept="image/*"
  maxSize={5_000_000}
  onUpload={async (upload, { signal, onProgress }) => {
    const body = new FormData()
    body.append('file', upload.file)

    const response = await fetch('/api/upload', { method: 'POST', body, signal })
    if (!response.ok) throw new Error(await response.text())

    onProgress(1)
    return response.json()
  }}
/>

Composer

Change the props and the source below follows.

No file selected
Props
tsx
<InputFile
  size="md"
  variant="default"
  multiple={true}
  showList={true}
  onUpload={async (upload, { signal, onProgress }) => {
    // upload.file, upload.name, upload.size, upload.type
    await putToApi(upload.file, { signal, onProgress })
  }}
/>

Examples

Uploading, with progress and a failure

Sizes

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

InputFile

Extends Omit< ComponentProps<'input'>, 'size' | 'type' | 'value' | 'onChange' | 'onSelect' >.

PropTypeDefaultDescription
placeholderstring'No file selected'Text shown when nothing is picked.
sizekeyof typeof fieldSize'md'The same field scales as Input, so the two line up in a form. The trigger is capped at sm, since the xs control is a true pill and reads wrong inside a field.
variant'default' | 'secondary' | 'ghost''default'The same field scales as Input, so the two line up in a form. The trigger is capped at sm, since the xs control is a true pill and reads wrong inside a field.
errorbooleanfalse—
buttonLabelReactNode'Browse'—
clearLabelReactNode'Clear'—
showListbooleantrueRenders UploadList underneath, with per-file progress, retry and remove.
onUploadUploadHandler—Runs the upload. Resolve to succeed — the value lands on upload.result; throw to fail, and the thrown message shows on the row with a retry.
onSelect(uploads: FileUpload[]) => void—Selection before any upload starts, and every transition after — for a form that needs the ids.
onUploadsChange(uploads: FileUpload[]) => void—Selection before any upload starts, and every transition after — for a form that needs the ids.
maxSizenumber—Rejected before the request is made, so an oversized file never leaves the browser.
maxSizeLabel(limit: string) => string—Rejected before the request is made, so an oversized file never leaves the browser.
isUploadingboolean—Force the busy state from outside, when the request is yours.
uploadingLabel(name: string) => string(name) => `Uploading ${name}…`Names the file being sent.

Types

FileUpload

The structured payload handed to onUpload. file is the browser File — put it straight in a FormData.

FieldTypeDescription
idstringStable across the file's whole life — use it as a React key.
fileFileThe browser's File. Put this straight into a FormData.
namestring—
sizenumberBytes.
typestringMIME type, from the browser. Empty string when it cannot tell.
lastModifiednumber—
statusUploadStatus—
progressnumber0–1. Only meaningful while status is 'uploading'.
result?unknownWhatever onUpload resolved to — an id, a URL, the parsed response.
error?string—

UploadControl

Pass the signal to fetch so removing a file aborts its request; call onProgress with 0–1 to drive the bar.

FieldTypeDescription
signalAbortSignalPass to fetch. Aborts on remove, replace or unmount.
onProgress(fraction: number) => voidReport 0–1. Without it the bar stays indeterminate, which is honest.
TypeDefinitionDescription
UploadHandler
(
  upload: FileUpload,
  control: UploadControl,
) => Promise<unknown> | unknown
—

Behaviour

TopicDetailDescription
accessibilityaria-labelledbyThe hidden native input stays the focusable control and is named by the visible summary; the ring is drawn on the wrapper with focus-within.