Yugen UI

FileUpload

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.

Uses a real <input type="file"> (visually hidden), so it is reachable with Tab, Enter / Space opens the file dialog, and the Field label points to the right control. accept / maxSize / maxFiles / validate also check dropped files; rejected files get a message (in Vietnamese) right below the drop zone (role="alert"). Each file is a FileUploadItem: a type icon or a thumbnail (URL.createObjectURL, revoked on removal), the name truncated with a title, the size, a ProgressBar while uploading, and Retry / Remove buttons. Added and removed files are announced through a polite live region (useAnnounce). FileUpload doesn't send files itself: upload them in onFilesChange and update status / progress (controlled mode).
You can pick several files at once.

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.

TSX
// 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.

Files start uploading as soon as you pick them.

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.

Front and back, as a PDF or images.

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

Do
  • 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
  • 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 · TypeDefaultDescription
acceptstringDefault: —Same as the native input: ".pdf,image/*". Also applies to dropped and pasted files.
multiplebooleanDefault: falseAllows several files. When off, a new file replaces the old one.
maxFilesnumberDefault: —Maximum total number of files (with multiple). Extra files are rejected with the too-many-files reason.
maxSizenumberDefault: —Maximum size per file (bytes).
validate(file: File) => string | null | undefinedDefault: —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 }) => voidDefault: —Called when files are added or removed.
onReject(rejections: FileUploadRejection[]) => voidDefault: —Rejected files: { file, code: file-invalid-type | file-too-large | too-many-files | custom, message }.
onRetry(entry: FileUploadEntry) => voidDefault: —When set, files with status="error" show a Retry button.
allowPastebooleanDefault: falseAccepts files pasted with Ctrl + V while the drop zone has focus.
showThumbnailsbooleanDefault: trueThumbnails for image files (created in an effect, revoked on removal or unmount).
label / descriptionReactNodeDefault: —Main line / secondary line in the drop zone.
disabled / requiredbooleanDefault: —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?) => booleanDefault: —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 · TypeDefault (English)messagesVi
dropzonestring

Dòng chính của vùng thả: dropzone + nút chữ browse (in đậm, màu link).

Default (English)"Drag files here or"messagesVi"Kéo thả tệp vào đây hoặc"
browsestringDefault (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 (image/* → "ảnh"…).

Default (English)"images"messagesVi"ảnh"
typeVideostringDefault (English)"videos"messagesVi"video"
typeAudiostringDefault (English)"audio"messagesVi"âm thanh"
typeTextstringDefault (English)"text"messagesVi"văn bản"
maxSizeHint(size: string) => string

Phần mô tả tự sinh; size là dung lượng đã định dạng.

Default (English)(size) => `up to ${size} per file`messagesVi(size) => `tối đa ${size} mỗi tệp`
maxFilesHint(count: number) => stringDefault (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; accepted là danh sách định dạng, size là dung lượng đã định dạng.

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) => stringDefault (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) => stringDefault (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 multiple.

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) => stringDefault (English)(count) => `Added ${count} files.`messagesVi(count) => `Đã thêm ${count} tệp.`
removed(name: string) => stringDefault (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}`
uploadingstringDefault (English)"Uploading…"messagesVi"Đang tải lên…"
uploadingPercent(percent: number) => stringDefault (English)(percent) => `Uploading… ${percent}%`messagesVi(percent) => `Đang tải lên… ${percent}%`
uploadedstringDefault (English)"Uploaded"messagesVi"Đã tải lên"
failedstringDefault (English)"Upload failed"messagesVi"Tải lên thất bại"
retry(name: string) => stringDefault (English)(name) => `Retry uploading ${name}`messagesVi(name) => `Thử lại tải lên ${name}`
remove(name: string) => stringDefault (English)(name) => `Remove ${name}`messagesVi(name) => `Xoá tệp ${name}`