# File Upload

> A React file upload with drag and drop, paste and camera capture: validation, previews, per-file progress, and list, grid or avatar layouts.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add file-upload`
- Docs: https://wingo-ui.com/components/file-upload

## Interaction

Drag files from your desktop over the page and every dropzone lights up with "Drop to upload"; over the zone itself it tints, grows a touch and its icon lifts. Click the dropzone (or focus it and press Enter or Space) to open the file picker, or paste a copied file or screenshot with Ctrl+V / Cmd+V while focus is inside. Each file is checked against the accepted types, the size limit and the file limit: rejected files are listed under the dropzone with the reason and never silently dropped. Accepted images show a preview right away. With an upload function each row fills a progress bar (a ring on photos and the avatar), at most three at a time by default, and pops a green check when it finishes; X cancels a running upload, the circular arrow retries a failed or canceled one, the trash can removes a file and aborts its upload. In the photo grid the first photo is the cover: drag a tile to reorder, or focus it and press Space, move it with the arrow keys and press Space again (Escape cancels); screen readers hear every move. On a phone the dropzone reads "Tap to choose files", remove buttons on photos are always visible, a 250ms press starts reordering so the page still scrolls, every action has a 44px target, and with camera on a "Take photo" button opens the camera.

## Installation

```bash
npx wingo-ui@latest add file-upload
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/file-upload` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/file-upload.tsx`

npm dependencies: `npm i @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { FileUpload, type UploadFile } from "@/components/ui/file-upload"

// your endpoint; signal aborts, onProgress fills the bar
function uploadFile(file: File, { signal, onProgress }: { signal: AbortSignal; onProgress: (fraction: number) => void }) {
  return new Promise<{ url: string }>((resolve, reject) => {
    const request = new XMLHttpRequest()
    request.open("POST", "/api/upload")
    request.upload.onprogress = (event) => onProgress(event.loaded / event.total)
    request.onload = () => (request.status < 300 ? resolve(JSON.parse(request.responseText)) : reject(new Error("Upload failed")))
    request.onerror = () => reject(new Error("No connection"))
    signal.addEventListener("abort", () => request.abort())
    const body = new FormData()
    body.append("file", file)
    request.send(body)
  })
}

const existing: UploadFile[] = [
  { id: "contract", name: "purchase-agreement.pdf", size: 1887437, type: "application/pdf", url: "/files/purchase-agreement.pdf", status: "done", progress: 1 },
]

export function PropertyDocuments() {
  return (
    <FileUpload
      label="Property documents"
      description="PDFs or images, up to 10 MB each"
      accept="image/*,.pdf"
      defaultValue={existing}
      upload={uploadFile}
      locale="en-US"
      labels={{ title: "Drop your documents here or", browse: "browse" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant?` | `"list" \| "grid" \| "avatar" \| "compact"` | `"list"` | list: a dropzone above a list of rows (documents). grid: photo tiles you can reorder, the first is the cover (listing photos). avatar: one round photo with a camera badge. compact: an attach button and small rows (chat, comments). |
| `accept?` | `string` | `""` | Accepted types, like the native attribute: "image/*,.pdf". Extensions match case-insensitively, wildcards match the MIME type. Also feeds the picker and the automatic hint. The exported acceptsFile(file, accept) runs the same check. |
| `maxSize?` | `number` | `"10485760"` | Largest file in bytes (10 MB); 0 means no limit. Bigger files are rejected with labels.tooLarge. |
| `maxFiles?` | `number` | `0` | How many files the list may hold; 0 means no limit. Extra files are rejected, not dropped. At the limit the dropzone turns off and shows "8 / 8". The avatar always holds one. |
| `multiple?` | `boolean` | `true` | Pick several files at once. Off, a new file replaces the current one. |
| `upload?` | `(file: File, context: { signal: AbortSignal; onProgress: (fraction: number) => void }) => Promise<unknown>` |  | Uploads one file. Pass signal to fetch so cancel, remove and unmount abort it, call onProgress with 0..1 (throttled to one update per 100ms per file). What it resolves with lands in the file's response; a rejection keeps the file with its error and a retry button. Without it files stay idle, for forms that submit them natively. |
| `autoUpload?` | `boolean` | `true` | Start uploads as files are added. Off, each row gets an upload button and the list an "Upload all" button. |
| `maxConcurrent?` | `number` | `3` | Uploads that run at once; the rest wait their turn. |
| `sortable?` | `boolean` | `true` | Grid only: drag tiles to reorder (the first is the cover). A 250ms press on touch, Space and the arrow keys on a keyboard. |
| `camera?` | `boolean` | `false` | On touch screens, adds a "Take photo" button that opens the back camera (capture="environment", images). |
| `paste?` | `boolean` | `true` | Paste files and screenshots from the clipboard while focus is inside the component. |
| `validate?` | `(file: File) => (string \| null \| undefined)` |  | Extra checks after type and size: return a message to reject the file with it (a duplicate, a too-small photo). formatFileSize(bytes, locale) is exported for messages. |
| `onReject?` | `(rejections: { file: File; reason: "type" \| "size" \| "count" \| "custom"; message: string }[]) => void` |  | Called with every rejected file of one add, together. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Dropzone padding and icon (list), the attach button (compact) and the avatar: 64 / 96 / 128px. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corners of the dropzone and photo tiles: 0 / 12 / 16 / 24px (full stays 24px). The avatar is always round. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Color of the drag-over tint, the progress bars and rings and the avatar badge. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone. Leave empty to use the tone. |
| `icon?` | `ReactNode` |  | The dropzone icon, CloudUpload by default. |
| `labels?` | `Partial<FileUploadLabels>` |  | Every string, English by default: title ("Drag files here or"), browse, tapTitle, dropHere, hint (else built from accept, maxSize and maxFiles), camera, add, attach, change, choose, cover, remove, retry, cancel, start, uploadAll, uploading, queued, uploaded, canceled, failed, limitReached, images / videos / audio, wrongType, tooLarge(max), tooMany(max), maxSizeHint(max, single), maxFilesHint(max), the announcements announceUploaded / announceFailed / announceRejected / announceRemoved and the reordering texts dragInstructions, dragStart, dragOver, dragEnd, dragCancel. |
| `locale?` | `string` | `""` | Formats file sizes ("2,4 MB" in ro-RO, "2.4 MB" in en-US). Empty uses the nearest LocaleProvider, then the page language. |
| `label?` | `ReactNode` | `""` | Renders the field anatomy around the component. A click on it focuses the dropzone without opening the picker. |
| `description?` | `ReactNode` | `""` | Helper text. In the list layout it becomes the dropzone's second line (unless labels.hint is set); elsewhere it shows under the component. |
| `error?` | `ReactNode` | `""` | The animated error under the component; also marks the dropzone invalid. |
| `required?` | `boolean` | `false` | The red * after the label; with name, the native form refuses to submit an empty list. |
| `invalid?` | `boolean` | `false` | The invalid look without a message. |
| `disabled?` | `boolean` | `false` | Dims everything and blocks picking, dropping, pasting and the row actions. |
| `name?` | `string` | `""` | Names the hidden file input. Its files follow the list (through DataTransfer), so a native form or FormData submits exactly the files shown. |
| `value?` | `UploadFile[]` |  | The files, controlled: { id, name, size, type, file?, url?, previewUrl?, status: "idle" \| "uploading" \| "done" \| "error" \| "canceled", progress, error?, response? }. Files that already exist on a server have url and status "done". Dropping a file mid-upload aborts it. |
| `defaultValue?` | `UploadFile[]` | `"[]"` | The starting files when uncontrolled; a form reset returns to them. |
| `onValueChange?` | `(files: UploadFile[]) => void` |  | The whole list after every add, remove, reorder, status change and throttled progress update. |
| `id?` | `string` |  | The main trigger's id (dropzone, add tile, avatar or attach button), which the label points to. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the field wrapper: optional, labelAction, orientation, hideLabel. |
| `classNames?` | `Partial<Record<"dropzone" \| "icon" \| "title" \| "hint" \| "camera" \| "rejections" \| "list" \| "item" \| "thumb" \| "name" \| "meta" \| "progress" \| "action" \| "grid" \| "tile" \| "cover" \| "remove" \| "add" \| "avatar" \| "input" \| "field" \| "label" \| "description" \| "error" \| "counter", string>>` |  | Extra classes for the inner parts and the field parts. |
| `className?` | `string` |  | Extra classes for the root (the field root when a label, description or error is set). |

## Parts

### formatFileSize (function)

formatFileSize(bytes, locale?): a short, localized size like "2.4 MB" or "830 KB". Handy for validate messages and your own file lists.

### acceptsFile (function)

acceptsFile(file, accept?): the same check as the accept prop (extensions, exact MIME types and "image/*" wildcards), for validating files outside the component.

### UploadFile (type)

One entry of value: { id, name, size, type, file?, url?, previewUrl?, status, progress, error?, response? }.

### UploadHandler (type)

(file, { signal, onProgress }) => Promise<unknown>: your upload function. Pass signal to fetch so cancel and remove abort it.

### FileRejection (type)

A refused file passed to onReject: { file, reason: "type" | "size" | "count" | "custom", message }.

### FileUploadProps (type)

The props of FileUpload, for wrappers that pass them through.

## Examples

- **Listing photos**: grid with maxFiles={8}: drag or use Space and the arrows to reorder, the first tile is the cover.
- **Avatar**: One round photo with a camera badge; a new pick replaces it and the ring shows the upload.
- **Compact in a chat composer**: An attach button with small rows, controlled so Send can wait for the uploads.
- **Validation**: Images only, 2 MB, at most 3, plus a duplicate check through validate. Try the three files.
- **Failures, retry and cancel**: Every third upload fails halfway; maxConcurrent={2} keeps the rest waiting.
- **Camera on phones**: camera adds a Take photo button on touch screens; desktops keep the plain dropzone.
- **Native form**: name="documents" and no upload: the files ride along in FormData, reset empties the list.
- **Info tone**: radius="lg" and the info tone on the progress, sizes as 1.8 MB.

## See also

- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Image Cropper](https://wingo-ui.com/components/image-cropper) ([Markdown](https://wingo-ui.com/components/image-cropper.md))
- [File types](https://wingo-ui.com/components/file-types) ([Markdown](https://wingo-ui.com/components/file-types.md))
- [Signature Pad](https://wingo-ui.com/components/signature-pad) ([Markdown](https://wingo-ui.com/components/signature-pad.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))

---

Canonical: https://wingo-ui.com/components/file-upload
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
