component · 24/108
File Upload
File selection and drag and drop with validation, progress, retry, and removal.
Install
Install manuallyView source on GitHub
Anatomy
- FileUpload
- FileUploadDropzone
- FileUploadInput
- FileUploadTrigger
- FileUploadList
- FileUploadItem
- FileUploadProgress
- FileUploadRetry
- FileUploadRemove
Accessibility3 checks
- Choose files is the keyboard alternative to dropping files. Both routes apply accept, maxSize, and count limits.
- Upload progress is a progressbar that exposes its value; provide a file-specific accessible label.
- The consumer owns upload requests, cancellation, progress, retry, removal, and server validation. fileCount includes existing files. Client checks are usability feedback, not a security boundary.
Forms and usability
Local demo: progress is simulated. Files are not sent to a server.
Code example
usage.tsx"use client";import { useEffect, useState } from "react";import { FileUpload, FileUploadDropzone, FileUploadInput, FileUploadItem, FileUploadList, FileUploadProgress, FileUploadRemove, FileUploadRetry, FileUploadTrigger, type FileUploadVariant,} from "@/components/ui/uai/file-upload";type DemoFile = { id: string; name: string; progress: number; status: "uploading" | "complete" | "error";};export function FileUploadPreview({ variant = "dropzone" }: { variant?: FileUploadVariant }) { const [files, setFiles] = useState<DemoFile[]>([ { id: "brief", name: "Q3 launch brief.pdf", progress: 100, status: "complete" }, { id: "deck", name: "pricing-review-deck.pdf", progress: 0, status: "error" }, ]); const uploading = files.some((file) => file.status === "uploading"); useEffect(() => { if (!uploading) return; const timer = setInterval( () => setFiles((current) => current.map((file) => file.status === "uploading" ? { ...file, progress: Math.min(100, file.progress + 20), status: file.progress >= 80 ? "complete" : "uploading", } : file, ), ), 350, ); return () => clearInterval(timer); }, [uploading]); return ( <FileUpload variant={variant} accept=".pdf,image/*" maxSize={5 * 1024 * 1024} maxFiles={5} fileCount={files.length} onFilesAccepted={(accepted) => setFiles((current) => [ ...current, ...accepted.map((file) => ({ id: crypto.randomUUID(), name: file.name, progress: 0, status: "uploading" as const, })), ]) } > <FileUploadDropzone> <div style={{ textAlign: variant === "dropzone" ? "center" : "left", flex: variant === "dropzone" ? undefined : 1, }} > <strong style={{ fontWeight: 500 }}>Add project files</strong> <p style={{ margin: "2px 0 0", fontSize: 12, color: "var(--uai-subtle)" }}> Drop PDFs or images · up to 5 MB each · 5 files </p> </div> <FileUploadInput /> <FileUploadTrigger /> </FileUploadDropzone> <FileUploadList> {files.map((file) => ( <FileUploadItem key={file.id} status={file.status} progress={file.progress}> <strong style={{ fontWeight: 500 }}>{file.name}</strong> <FileUploadProgress aria-label={`Uploading ${file.name}`} /> <div style={{ display: "flex", gap: 4 }}> <FileUploadRetry onClick={() => setFiles((current) => current.map((item) => item.id === file.id ? { ...item, progress: 0, status: "uploading" } : item, ), ) } /> <FileUploadRemove aria-label={`Remove file ${file.name}`} onClick={() => setFiles((current) => current.filter((item) => item.id !== file.id))} /> </div> </FileUploadItem> ))} </FileUploadList> <p style={{ margin: 0, fontSize: 11.5, color: "var(--uai-subtle)" }}> Local demo: progress is simulated. Files are not sent to a server. </p> </FileUpload> );}Manual installation
file-upload.tsxLoading component source…