Dropzone
The upload card: click it or drag onto it, and it 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/dropzone.tsxAbout
From the component's own source, where it was written.
The upload card: click it or drag onto it, and it runs the upload.
Drag and drop is a pointer-only affordance, so the card is a real <button> wrapping a visually hidden <input type="file"> — keyboard and screen-reader users get the file picker, everyone else can drag onto it. A div with a drop handler and no keyboard path is the usual version of this component and it is unusable without a mouse.
dragDepth counts enter/leave rather than tracking a boolean: dragging over a child fires dragleave on the parent, so a boolean flickers the highlight off every time the pointer crosses the icon inside the card.
The upload itself lives in useUploads, shared with InputFile. Hand this component onUpload and it moves each file through queued → uploading → done or error, reports progress, and keeps failures on screen with a retry. While anything is in flight the card swaps to a spinner naming the file, so the component that took the drop is also the one that says what happened to it.
Validation runs on drop as well as on pick. The browser filters the file picker by accept, but a dropped file never went through the picker — so without a check here, dragging a .mov onto an image-only zone uploads it.
Install
npx astralyx-ui add dropzone
Usage
import { Dropzone } from '@/components/ui/dropzone'
<Dropzone
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.
<Dropzone
multiple={true}
showList={true}
accept="image/*"
maxSize={5_000_000}
onUpload={async (upload, { signal, onProgress }) => {
await putToApi(upload.file, { signal, onProgress })
}}
/>Examples
Uploading, with progress and retry
<Dropzone multiple onUpload={upload} />When the endpoint refuses it
<Dropzone onUpload={upload} accept="image/*" maxSize={2_000_000} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
Dropzone
Extends Omit<ComponentProps<'div'>, 'onChange' | 'onSelect'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| onUpload | UploadHandler | — | Runs the upload. Resolve to succeed — the value lands on upload.result; throw to fail, and the message shows on the row with a retry. |
| onSelect | (uploads: FileUpload[]) => void | — | Fires on selection, before any upload starts. |
| onUploadsChange | (uploads: FileUpload[]) => void | — | Fires on every transition — start, progress, finish, failure, removal. |
| accept | string | — | Both are enforced on drop as well as on pick — a dropped file never went through the picker, so the browser never filtered it. |
| multiple | boolean | false | Both are enforced on drop as well as on pick — a dropped file never went through the picker, so the browser never filtered it. |
| disabled | boolean | false | — |
| maxSize | number | — | Bytes. Rejected before the request is made, so an oversized file never leaves the browser. |
| label | ReactNode | 'Click to upload, or drag and drop' | — |
| hint | ReactNode | — | — |
| showList | boolean | true | Show the per-file rows under the card. |
| isUploading | boolean | — | Forces the busy state, for when the request is yours rather than onUpload’s. Shows the spinner and blocks further picking. |
| uploadingLabel | (name: string) => ReactNode | (name) => `Uploading ${name}…` | The card names the file in flight and reports the total when it settles. |
| doneLabel | (count: number) => ReactNode | (count) => `${count} file${count === 1 ? '' : 's'} uploaded` | The card names the file in flight and reports the total when it settles. |
| maxSizeLabel | (limit: string) => string | — | — |
| acceptLabel | (accept: string) => string | — | — |
Types
FileUpload
The structured payload handed to onUpload. file is the browser File — put it straight into 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 | button + sr-only input | The card is a real button wrapping a hidden file input, so there is one tab stop and drag-and-drop is never the only way in. |