File Upload
FileUpload is a drop zone with a file list: pick with mouse or keyboard, drag and drop or paste; type, size and count are validated.
Import
Install from npm, copy the source into your project with @yugen-ui/cli or the shadcn CLI, or import directly when working in this repo.
// npm install yugen-uiimport { FileUpload, FileUploadItem, createFileEntry, isFileAccepted, type FileUploadEntry } from "yugen-ui";Variants
Each tile is a standalone example: hover to copy, or press Code to see the source.
Upload with progress, errors and Retry
Controlled mode: files + onFilesChange. Each new file is uploaded right away, updating progress (0 → 1) and status. Turn on “Simulate a network error” to see the Retry button.
Single image with thumbnail and clipboard paste
multiple defaults to false: picking a new image replaces the old one. allowPaste accepts images pasted with Ctrl + V while the drop zone has focus.
Required inside a Field
Field provides the label, hint and validationMessage; aria-invalid switches the drop zone border to the error color. Click “Submit” without picking a file.
Usage
- State the allowed formats and size before people pick files (generated from accept / maxSize, or set through description).
- Show progress for large files and always offer Retry when an upload fails, so people don't have to start over.
- Wrap it in a Field so the label, hint and error message are linked to the control.
- Don't rely on accept on the client alone. The server still has to check file type and size.
- Don't use FileUpload for a small “Attach” button in a message composer. Use PromptInput, or a Button with a hidden input.
- Don't submit the form as soon as files are picked; let people review the list first.
Props
| Prop · Type | Default | Description |
|---|---|---|
| acceptstring | Default: — | Same as the native input: ".pdf,image/*". Also applies to dropped and pasted files. |
| multipleboolean | Default: false | Allows several files. When off, a new file replaces the old one. |
| maxFilesnumber | Default: — | Maximum total number of files (with multiple). Extra files are rejected with the too-many-files reason. |
| maxSizenumber | Default: — | Maximum size per file (bytes). |
| validate(file: File) => string | null | undefined | Default: — | Custom check; return an error string to reject the file. |
| files / defaultFilesFileUploadEntry[] | Default: — | File list (controlled / initial). Entry: { id, name, size, type?, file?, previewUrl?, status?, progress?, error? }. |
| onFilesChange(files, { added, removed }) => void | Default: — | Called when files are added or removed. |
| onReject(rejections: FileUploadRejection[]) => void | Default: — | Rejected files: { file, code: file-invalid-type | file-too-large | too-many-files | custom, message }. |
| onRetry(entry: FileUploadEntry) => void | Default: — | When set, files with status="error" show a Retry button. |
| allowPasteboolean | Default: false | Accepts files pasted with Ctrl + V while the drop zone has focus. |
| showThumbnailsboolean | Default: true | Thumbnails for image files (created in an effect, revoked on removal or unmount). |
| label / descriptionReactNode | Default: — | Main line / secondary line in the drop zone. |
| disabled / requiredboolean | Default: — | Passed by Field. required sets aria-required (the native required isn't used because the input value is cleared after each pick). |
| refRef<HTMLInputElement> | Default: — | Points to the <input type="file">: ref.current.click() opens the dialog, and focus() works with react-hook-form. |
| FileUploadItemcomponent (<li>) | Default: — | File row you can use on its own: name, size, type, file, previewUrl, status, progress, error, onRemove, onRetry. |
| createFileEntry / isFileAccepted(file, init?) => FileUploadEntry / (file, accept?) => boolean | Default: — | Helpers to create an entry and to check accept. Sizes are formatted with formatFileSize (shared with PromptInput). |
Messages
Text built into the component: labels, aria-labels, screen reader announcements.
English by default. Change it with the messages prop of YugenProvider, by the namespace below; messagesVi has the Vietnamese set. See Interface language.
messages.fileUpload
| Key · Type | Default (English) | messagesVi |
|---|---|---|
| dropzonestring Dòng chính của vùng thả: | Default (English)"Drag files here or" | messagesVi"Kéo thả tệp vào đây hoặc" |
| browsestring | Default (English)"browse" | messagesVi"chọn tệp" |
| droppingstring Dòng chính khi đang kéo tệp vào vùng thả. | Default (English)"Drop files to upload" | messagesVi"Thả tệp để tải lên" |
| selectedFilesstring aria-label danh sách tệp đã chọn. | Default (English)"Selected files" | messagesVi"Tệp đã chọn" |
| typeImagestring Tên nhóm MIME trong mô tả định dạng ( | Default (English)"images" | messagesVi"ảnh" |
| typeVideostring | Default (English)"videos" | messagesVi"video" |
| typeAudiostring | Default (English)"audio" | messagesVi"âm thanh" |
| typeTextstring | Default (English)"text" | messagesVi"văn bản" |
| maxSizeHint(size: string) => string Phần mô tả tự sinh; | Default (English)(size) => `up to ${size} per file` | messagesVi(size) => `tối đa ${size} mỗi tệp` |
| maxFilesHint(count: number) => string | Default (English)(count) => (count === 1 ? "up to 1 file" : `up to ${count} files`) | messagesVi(count) => `tối đa ${count} tệp` |
| invalidType(name: string, accepted: string) => string Lỗi từ chối tệp; | Default (English)(name, accepted) => `"${name}" isn't a supported file type (accepted: ${accepted}).` | messagesVi(name, accepted) => `"${name}" không đúng định dạng (chỉ nhận ${accepted}).` |
| tooLarge(name: string, size: string) => string | Default (English)(name, size) => `"${name}" is larger than the ${size} limit.` | messagesVi(name, size) => `"${name}" vượt quá dung lượng tối đa ${size}.` |
| tooMany(name: string, limit: number) => string | Default (English)(name, limit) => `You can select up to ${limit === 1 ? "1 file" : `${limit} files`}. "${name}" was skipped.` | messagesVi(name, limit) => `Chỉ được chọn tối đa ${limit} tệp — đã bỏ qua "${name}".` |
| onlyOne(name: string) => string Khi không bật | Default (English)(name) => `You can select only 1 file. "${name}" was skipped.` | messagesVi(name) => `Chỉ được chọn 1 tệp — đã bỏ qua "${name}".` |
| added(name: string) => string Thông báo (aria-live) khi thêm / xoá tệp. | Default (English)(name) => `Added ${name}.` | messagesVi(name) => `Đã thêm tệp ${name}.` |
| addedCount(count: number) => string | Default (English)(count) => `Added ${count} files.` | messagesVi(count) => `Đã thêm ${count} tệp.` |
| removed(name: string) => string | Default (English)(name) => `Removed ${name}.` | messagesVi(name) => `Đã xoá tệp ${name}.` |
| uploadProgress(name: string) => string FileUploadItem. | Default (English)(name) => `Upload progress for ${name}` | messagesVi(name) => `Tiến trình tải lên ${name}` |
| uploadingstring | Default (English)"Uploading…" | messagesVi"Đang tải lên…" |
| uploadingPercent(percent: number) => string | Default (English)(percent) => `Uploading… ${percent}%` | messagesVi(percent) => `Đang tải lên… ${percent}%` |
| uploadedstring | Default (English)"Uploaded" | messagesVi"Đã tải lên" |
| failedstring | Default (English)"Upload failed" | messagesVi"Tải lên thất bại" |
| retry(name: string) => string | Default (English)(name) => `Retry uploading ${name}` | messagesVi(name) => `Thử lại tải lên ${name}` |
| remove(name: string) => string | Default (English)(name) => `Remove ${name}` | messagesVi(name) => `Xoá tệp ${name}` |