File Upload
A composable file upload solution with drag-and-drop, validation, previews, and optional upload progress.
Overview
The File Upload system gives you two ways to build file upload experiences: use FileUploadField for a ready-to-use component, or combine useFileUpload with the FileUpload.* primitives for fully custom UIs.
Import
// Recommended: ready-to-use component
import { FileUploadField } from "@px-ui/forms";
// Advanced: hook + composable primitives
import { FileUpload, useFileUpload } from "@px-ui/core";
// File type icons
import { getFileTypeIcon } from "@px-ui/core";Usage
This example uses FileUploadField, which includes state management, validation, and optional upload support.
import { FileUploadField } from "@px-ui/forms";
export function Uploader() {
return (
<FileUploadField
accept="image/*,.pdf,.doc,.docx"
maxSize={10 * 1024 * 1024}
onFilesChange={(files) => console.log(files)}
/>
);
}Examples
With S3/presigned uploads
Configure uploads by providing upload.getPresignedUrl and upload.uploadFile. Progress is handled for you when uploadFile calls onProgress.
import { FileUploadField } from "@px-ui/forms";
export function UploaderWithUpload() {
return (
<FileUploadField
accept="image/*,.pdf"
multiple
maxFiles={5}
maxSize={5 * 1024 * 1024}
upload={{
getPresignedUrl: async ({ filename, contentType, size }) => {
const res = await fetch("/api/get-upload-url", {
method: "POST",
body: JSON.stringify({ filename, contentType, size }),
});
const data = await res.json();
return {
result: {
url: data.url,
fullPath: data.fullPath,
},
};
},
uploadFile: async (url, file, presignedData, onProgress) => {
// Implement upload to `url` and call `onProgress(0..100)`.
// Return the final URL (or rely on `presignedData.fullPath`).
await fetch(url, { method: "PUT", body: file });
onProgress?.(100);
return { result: { url: presignedData.fullPath } };
},
}}
/>
);
}Fully custom UI (hook + primitives)
Use useFileUpload for state/actions and pass the entire hook return directly into FileUpload.Root.
import { FileUpload, useFileUpload } from "@px-ui/core";
export function CustomDropzone() {
const upload = useFileUpload({
onFilesChange: (files) => console.log(files),
});
return (
<FileUpload.Root upload={upload}>
<FileUpload.Dropzone />
</FileUpload.Root>
);
}Avatar upload (render prop)
Use the render prop for complete control over the dropzone element. The render function receives props (event handlers to spread) and state (current upload state).
Click or drag to upload avatar
import { FileUploadField } from "@px-ui/forms";
export function AvatarUpload() {
return (
<FileUploadField
accept="image/*"
maxSize={2 * 1024 * 1024}
showFileList={false}
render={(props, state) => (
<div
{...props}
className={`flex size-32 cursor-pointer items-center justify-center rounded-full bg-neutral-100 transition-colors ${
state.isDragging ? "border-2 border-dashed border-blue-500" : ""
}`}
>
{state.files.length > 0 ? (
<img
src={state.files[0].preview}
alt="Avatar"
className="size-full rounded-full object-cover"
/>
) : (
<div className="flex flex-col items-center justify-center text-neutral-500">
<UserIcon className="size-10" />
<span className="mt-1 text-xs">Upload</span>
</div>
)}
</div>
)}
/>
);
}Form reset integration
Use reset() and instanceKey for proper form reset handling.
import { FileUpload, useFileUpload, Button } from "@px-ui/core";
export function FormResetDemo() {
const [name, setName] = useState("");
const upload = useFileUpload({
multiple: true,
upload: { getPresignedUrl, uploadFile },
onFilesChange: (files) => console.log("Files changed:", files),
});
const handleSubmit = (e) => {
e.preventDefault();
console.log("Submitted:", { name, files: upload.files });
};
const handleReset = () => {
setName("");
upload.reset(); // Clears files, cancels uploads, increments instanceKey
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/* Use instanceKey to force remount on reset */}
<FileUpload.Root key={upload.instanceKey} upload={upload} multiple>
<FileUpload.Dropzone size="sm" />
<FileUpload.List>
{(files) => files.map((file) => (
<FileUpload.ListItem key={file.id} file={file} />
))}
</FileUpload.List>
</FileUpload.Root>
<Button type="submit">Submit</Button>
<Button type="button" onClick={handleReset}>Reset</Button>
</form>
);
}File Type Icons
The library includes file type icons that automatically match based on file extension or MIME type.
import { getFileTypeIcon } from "@px-ui/core";
// Get the icon component for a file
const Icon = getFileTypeIcon({ name: "document.pdf", type: "application/pdf" });
// Use it like any React component
<Icon className="size-5" />Available icons: PdfIcon, WordIcon, ExcelIcon, ImageIcon, VideoIcon, AudioIcon, ArchiveIcon, CodeIcon, TextIcon, GenericFileIcon.
Anatomy
The file upload primitives are designed to be composed together:
FileUpload.Root: Provides context for all child components (acceptsuploadprop)FileUpload.Dropzone: Drag-and-drop area (also supports paste and keyboard)FileUpload.List: Container for file items (render prop receives files)FileUpload.ListItem: Consolidated file row with icon, name, size, progress, and actionsFileUpload.HiddenInput: Hidden file input (if you need it separately from Dropzone)FileUpload.ClearAll: Clear all selected files button
API Reference
FileUploadField
A ready-to-use upload component that internally uses useFileUpload and the FileUpload.* primitives.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "default" | Dropzone size |
dropzoneText | string | "Paste Or Drag & Drop Files Here" | Dropzone label text |
buttonText | string | "Browse for files" | Trigger button label |
showFileList | boolean | true | Show file list below the control |
initialFiles | Array<{ id: string; name: string; size: number; type: string; url: string }> | [] | Pre-populate with already-uploaded files |
disabled | boolean | false | Disable all interactions |
renderFileItem | (file, actions) => React.ReactNode | - | Custom renderer for list items |
onError | (error: { type: string; message: string; files?: File[] }) => void | - | Validation errors callback |
Also accepts hook options: accept, multiple, maxSize, maxFiles, onFilesChange, onFileAdd, onFileRemove, and upload.
useFileUpload
A hook that manages file selection, validation, previews, and (optionally) uploads.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
accept | string | "*" | Accepted file types (e.g. "image/*,.pdf") |
multiple | boolean | false | Allow multiple files |
maxSize | number | Infinity | Max file size in bytes |
maxFiles | number | Infinity | Max file count (only when multiple) |
initialFiles | FileMetadata[] | [] | Pre-populate with uploaded files |
onFilesChange | (files) => void | - | Called when files change |
onFileAdd | (file) => void | - | Called when a file is added |
onFileRemove | (file) => void | - | Called when a file is removed |
upload | UploadConfig | - | Enables uploads and progress tracking |
Returns: UseFileUploadReturn
- State:
files,isDragging,errors,isUploading - Core Actions:
addFiles,removeFile,clearFiles,clearErrors - Form Integration:
reset(newFiles?),setFiles(files | (prev) => files),instanceKey - Upload Actions:
uploadFiles,retryUpload,cancelUpload - Drag/Drop + Input:
handleDragEnter,handleDragLeave,handleDragOver,handleDrop,handleFileChange,openFileDialog,getInputProps
FileUpload.Root
Context provider - accepts the entire hook return value.
| Prop | Type | Required | Description |
|---|---|---|---|
upload | UseFileUploadReturn | Yes | The return value from useFileUpload() |
accept | string | No | Accepted types (used by Dropzone/input) |
multiple | boolean | No | Allow multiple file selection |
disabled | boolean | No | Disable all interactions |
className | string | No | Additional CSS classes |
FileUpload.Dropzone
Drag-and-drop area (also supports paste, click, and keyboard Enter/Space).
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "default" | Dropzone size |
dropzoneText | string | "Paste Or Drag & Drop Files Here" | Primary label |
browseText | string | "Browse for files" | Button label |
render | (props: DropzoneRenderProps, state: DropzoneState) => ReactElement | - | Render prop for complete customization |
Inherited Props: All native div props (when not using render).
DropzoneRenderProps
Props passed to your custom element (spread these on your element):
interface DropzoneRenderProps {
onDragEnter: React.DragEventHandler;
onDragLeave: React.DragEventHandler;
onDragOver: React.DragEventHandler;
onDrop: React.DragEventHandler;
onPaste: React.ClipboardEventHandler;
onKeyDown: React.KeyboardEventHandler;
onClick: React.MouseEventHandler;
tabIndex: number;
role: string;
"aria-disabled": boolean;
"data-dragging": boolean;
}DropzoneState
State passed to your render function for conditional styling:
interface DropzoneState {
isDragging: boolean;
isUploading: boolean;
disabled: boolean;
files: FileUploadItem[];
}FileUpload.List
Renders nothing when there are no files.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((files) => ReactNode) | - | Render prop receives the current files |
Inherited Props: All native div props.
FileUpload.ListItem
Consolidated file row component that replaces all the individual Item* parts.
| Prop | Type | Default | Description |
|---|---|---|---|
file | FileUploadItem | - | The file object (required) |
showIcon | boolean | true | Show file type icon |
showProgress | boolean | true | Show progress bar when uploading |
showError | boolean | true | Show error message when status is error |
showRemove | boolean | true | Show remove button |
showRetry | boolean | true | Show retry button when status is error |
icon | React.ReactNode | - | Custom icon (overrides auto-detected icon) |
Inherited Props: All native div props.
FileUpload.HiddenInput
Hidden file input, useful if you need the input separate from the Dropzone.
Inherited Props: All native input props (except type).
FileUpload.ClearAll
Button to clear all selected files. Renders nothing when there are no files.
Inherited Props: All Button props.