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.tsxAbout
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.
<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
<InputFile multiple onUpload={upload} />Sizes
<InputFile size="sm" /> <InputFile multiple buttonLabel="Choose files" /> <InputFile variant="secondary" size="lg" />
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' >.
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder | string | 'No file selected' | Text shown when nothing is picked. |
| size | keyof 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. |
| error | boolean | false | — |
| buttonLabel | ReactNode | 'Browse' | — |
| clearLabel | ReactNode | 'Clear' | — |
| showList | boolean | true | Renders UploadList underneath, with per-file progress, retry and remove. |
| onUpload | UploadHandler | — | 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. |
| maxSize | number | — | 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. |
| isUploading | boolean | — | 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.
| Field | Type | Description |
|---|---|---|
| id | string | Stable across the file's whole life — use it as a React key. |
| file | File | The browser's File. Put this straight into a FormData. |
| name | string | — |
| size | number | Bytes. |
| type | string | MIME type, from the browser. Empty string when it cannot tell. |
| lastModified | number | — |
| status | UploadStatus | — |
| progress | number | 0–1. Only meaningful while status is 'uploading'. |
| result? | unknown | Whatever 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.
| Field | Type | Description |
|---|---|---|
| signal | AbortSignal | Pass to fetch. Aborts on remove, replace or unmount. |
| onProgress | (fraction: number) => void | Report 0–1. Without it the bar stays indeterminate, which is honest. |
| Type | Definition | Description |
|---|---|---|
UploadHandler | ( upload: FileUpload, control: UploadControl, ) => Promise<unknown> | unknown | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| accessibility | aria-labelledby | The hidden native input stays the focusable control and is named by the visible summary; the ring is drawn on the wrapper with focus-within. |