File Upload React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React file upload with drag and drop, paste and camera capture: validation, previews, per-file progress, and list, grid or avatar layouts.
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.
File Upload is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add file-upload, together with Field, Button, Badge, Number format, useLocale and 5 more and the npm packages @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add file-uploadFile Upload is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import FileUpload from @/components/ui/file-upload.
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
variantdefault"list""list" | "grid" | "avatar" | "compact"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).
acceptdefault""stringAccepted 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.
maxSizedefault"10485760"numberLargest file in bytes (10 MB); 0 means no limit. Bigger files are rejected with labels.tooLarge.
maxFilesdefault0numberHow 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.
multipledefaulttruebooleanPick 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.
autoUploaddefaulttruebooleanStart uploads as files are added. Off, each row gets an upload button and the list an "Upload all" button.
maxConcurrentdefault3numberUploads that run at once; the rest wait their turn.
sortabledefaulttruebooleanGrid only: drag tiles to reorder (the first is the cover). A 250ms press on touch, Space and the arrow keys on a keyboard.
cameradefaultfalsebooleanOn touch screens, adds a "Take photo" button that opens the back camera (capture="environment", images).
pastedefaulttruebooleanPaste 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 }[]) => voidCalled with every rejected file of one add, together.
sizedefault"md""sm" | "md" | "lg"Dropzone padding and icon (list), the attach button (compact) and the avatar: 64 / 96 / 128px.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corners of the dropzone and photo tiles: 0 / 12 / 16 / 24px (full stays 24px). The avatar is always round.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Color of the drag-over tint, the progress bars and rings and the avatar badge.
colordefault""stringAny CSS color; replaces the tone. Leave empty to use the tone.
iconReactNodeThe dropzone icon, CloudUpload by default.
labelsPartial<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.
localedefault""stringFormats file sizes ("2,4 MB" in ro-RO, "2.4 MB" in en-US). Empty uses the nearest LocaleProvider, then the page language.
labeldefault""ReactNodeRenders the field anatomy around the component. A click on it focuses the dropzone without opening the picker.
descriptiondefault""ReactNodeHelper text. In the list layout it becomes the dropzone's second line (unless labels.hint is set); elsewhere it shows under the component.
errordefault""ReactNodeThe animated error under the component; also marks the dropzone invalid.
requireddefaultfalsebooleanThe red * after the label; with name, the native form refuses to submit an empty list.
invaliddefaultfalsebooleanThe invalid look without a message.
disableddefaultfalsebooleanDims everything and blocks picking, dropping, pasting and the row actions.
namedefault""stringNames the hidden file input. Its files follow the list (through DataTransfer), so a native form or FormData submits exactly the files shown.
valueUploadFile[]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.
defaultValuedefault"[]"UploadFile[]The starting files when uncontrolled; a form reset returns to them.
onValueChange(files: UploadFile[]) => voidThe whole list after every add, remove, reorder, status change and throttled progress update.
idstringThe main trigger's id (dropzone, add tile, avatar or attach button), which the label points to.
fieldPropsPartial<FieldProps>Anything else for the field wrapper: optional, labelAction, orientation, hideLabel.
classNamesPartial<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.
classNamestringExtra classes for the root (the field root when a label, description or error is set).
Parts
formatFileSize
functionformatFileSize(bytes, locale?): a short, localized size like "2.4 MB" or "830 KB". Handy for validate messages and your own file lists.
acceptsFile
functionacceptsFile(file, accept?): the same check as the accept prop (extensions, exact MIME types and "image/*" wildcards), for validating files outside the component.
UploadFile
typeOne 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
typeA refused file passed to onReject: { file, reason: "type" | "size" | "count" | "custom", message }.
FileUploadProps
typeThe 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.
Dependencies
Files
- components/ui/file-upload.tsx
FAQ
Is the React File Upload component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install File Upload?
Run npx wingo-ui@latest add file-upload. It also adds the Wingo UI items it builds on (Field, Button, Badge, Number format, useLocale and 5 more) and the npm packages @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/file-upload.
How does File Upload behave on phones?
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.
See also
Used in
More React input components
Articles using this component
- React File Manager: Folders, Uploads and the Backend API
Component guides10 min read
- React PDF Viewer: Search, Highlights, Real Annotations
Component guides9 min read
- React CSV Importer: Upload, Map Columns, Fix Rows, Import
Component guides7 min read
- React File Upload: Drag and Drop, Paste, Camera, Progress
Component guides11 min read