FileUpload
A comprehensive file upload component with drag & drop functionality, progress tracking, and extensive customization options. Available as a simplified component or individual components for complex use cases.
Basic example
SVG, PNG, JPG or GIF (max. N/A)
const [files, setFiles] = useState<UploadedFileItemProps[]>([]);
const handleFilesAdded = (newFiles: File[]) => {
const newFilesWithIds = newFiles.map(file => ({
id: Math.random().toString(),
name: file.name,
size: file.size,
type: file.type as FileType,
progress: 0,
}));
setFiles([...newFilesWithIds, ...files]);
newFiles.forEach((file, index) => {
const fileObject = newFiles.find(
f =>
f.name === newFilesWithIds.find(nf => nf.id === newFilesWithIds[index].id)?.name
);
if (fileObject) {
simulateUploadFile( progress => {
setFiles(prev =>
prev.map(f => (f.id === newFilesWithIds[index].id ? { ...f, progress } : f))
);
});
}
});
};
const handleDeleteFile = (id: string) => {
setFiles(prev => prev.filter(f => f.id !== id));
};
const handleRetryFile = (id: string) => {
const file = files.find(f => f.id === id);
if (!file) return;
simulateUploadFile( progress => {
setFiles(prev => prev.map(f => (f.id === id ? { ...f, progress, failed: false } : f)));
});
};
return (
<FileUpload
variant="progress-bar"
files={files}
onFilesAdded={handleFilesAdded}
onDeleteFile={handleDeleteFile}
onRetryFile={handleRetryFile}
/>
);Progress variants
Variant: progress-bar - Shows progress with a traditional progress bar below file info
SVG, PNG, JPG or GIF (max. N/A)
Example dashboard screenshot.jpg
720 KB
Uploading...
50%Tech design requirements_2.pdf
720 KB
Complete
100%Tech motion requirements.pdf
1 MB
Failed
const [files, setFiles] = useState<UploadedFileItemProps[]>(placeholderFiles);
const handleFilesAdded = (newFiles: File[]) => {
const newFilesWithIds = newFiles.map(file => ({
id: Math.random().toString(),
name: file.name,
size: file.size,
type: file.type as FileType,
progress: 0,
}));
setFiles([...newFilesWithIds, ...files]);
newFiles.forEach((file, index) => {
const fileObject = newFiles.find(
f =>
f.name === newFilesWithIds.find(nf => nf.id === newFilesWithIds[index].id)?.name
);
if (fileObject) {
simulateUploadFile( progress => {
setFiles(prev =>
prev.map(f => (f.id === newFilesWithIds[index].id ? { ...f, progress } : f))
);
});
}
});
};
const handleDeleteFile = (id: string) => {
setFiles(prev => prev.filter(f => f.id !== id));
};
const handleRetryFile = (id: string) => {
const file = files.find(f => f.id === id);
if (!file) return;
simulateUploadFile( progress => {
setFiles(prev => prev.map(f => (f.id === id ? { ...f, progress, failed: false } : f)));
});
};
return (
<FileUpload
files={files}
variant="progress-bar"
onFilesAdded={handleFilesAdded}
onDeleteFile={handleDeleteFile}
onRetryFile={handleRetryFile}
/>
);Variant: progress-fill - Shows progress with a background fill effect
SVG, PNG, JPG or GIF (max. N/A)
Example dashboard screenshot.jpg
720 KB
50%
Tech design requirements_2.pdf
720 KB
100%
Tech motion requirements.pdf
Upload failed, please try again
const [files, setFiles] = useState<UploadedFileItemProps[]>(placeholderFiles);
const handleFilesAdded = (newFiles: File[]) => {
const newFilesWithIds = newFiles.map(file => ({
id: Math.random().toString(),
name: file.name,
size: file.size,
type: file.type as FileType,
progress: 0,
}));
setFiles([...newFilesWithIds, ...files]);
newFiles.forEach((file, index) => {
const fileObject = newFiles.find(
f =>
f.name === newFilesWithIds.find(nf => nf.id === newFilesWithIds[index].id)?.name
);
if (fileObject) {
simulateUploadFile( progress => {
setFiles(prev =>
prev.map(f => (f.id === newFilesWithIds[index].id ? { ...f, progress } : f))
);
});
}
});
};
const handleDeleteFile = (id: string) => {
setFiles(prev => prev.filter(f => f.id !== id));
};
const handleRetryFile = (id: string) => {
const file = files.find(f => f.id === id);
if (!file) return;
simulateUploadFile( progress => {
setFiles(prev => prev.map(f => (f.id === id ? { ...f, progress, failed: false } : f)));
});
};
return (
<FileUpload
files={files}
variant="progress-fill"
onFilesAdded={handleFilesAdded}
onDeleteFile={handleDeleteFile}
onRetryFile={handleRetryFile}
/>
);Form example
const fileUploadSchema = z.object({
documents: z.array(z.file()).min(1, "Upload at least one doc"),
profileImage: z.array(z.file()).nonempty("Upload an image for your profile"),
});
type FormData = z.infer<typeof fileUploadSchema>;
const form = useForm<FormData>({
resolver: zodResolver(fileUploadSchema),
defaultValues: {
documents: [],
profileImage: [],
},
});
const onSubmit = async (data: FormData) => {
const fileContent: {
documents: string[];
// CHANGEMENT : 'profileImage' est une chaîne simple car maxFiles=1
profileImage: string;
} = { documents: [], profileImage: "" };
const documentPromises = data.documents.map(async (file: File) => {
return `Nom: ${file.name}, Taille: ${file.size} octets`;
});
fileContent.documents = await Promise.all(documentPromises);
const profileFile = data.profileImage[0];
if (profileFile) {
fileContent.profileImage = `Nom: ${profileFile.name}, Taille: ${profileFile.size} octets`;
}
toast("You submitted the following values", {
description: (
<pre className="mt-2 w-[600px] rounded-md bg-neutral-950 p-4">
<code className="text-white">{JSON.stringify(fileContent, null, 2)}</code>
</pre>
),
});
console.log("File content data:", fileContent);
};
form.watch("profileImage");
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="flex items-start gap-10">
<ProfilImagePreview
file={form.getValues("profileImage")[0]}
className="size-24 rounded-full"
/>
<FileUploadForm
control={form.control}
name="profileImage"
label="Photo de profil"
isRequired
description="Upload your profile image"
accept="image/*"
maxSize={5 * 1024 * 1024} // 5MB
allowsMultiple={false}
hint="PNG, JPG or JPEG (max. 5MB)"
transformFiles={async files =>
files.map(file => createFileItem(file, { progress: 0 }))
}
/>
</div>
<FileUploadForm
control={form.control}
name="documents"
label="Documents"
hint="Upload your documents (PDF, DOC, DOCX)"
isRequired
accept=".pdf,.doc,.docx,application/pdf"
maxSize={10 * 1024 * 1024} // 10MB
maxFiles={5}
variant="progress-bar"
transformFiles={async files =>
files.map(file => createFileItem(file, { progress: 0 }))
}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);File type restrictions
Accept: image/* - Only image files are accepted
Please upload PNG or JPEG images only.
const [files, setFiles] = useState<UploadedFileItemProps[]>([]);
const handleFilesAdded = (newFiles: File[]) => {
const newFilesWithIds = newFiles.map(file => ({
id: Math.random().toString(),
name: file.name,
size: file.size,
type: file.type as FileType,
progress: 0,
}));
setFiles([...newFilesWithIds, ...files]);
newFiles.forEach((file, index) => {
const fileObject = newFiles.find(
f =>
f.name === newFilesWithIds.find(nf => nf.id === newFilesWithIds[index].id)?.name
);
if (fileObject) {
simulateUploadFile( progress => {
setFiles(prev =>
prev.map(f => (f.id === newFilesWithIds[index].id ? { ...f, progress } : f))
);
});
}
});
};
const handleUnacceptedFiles = (files: FileList) => {
console.log("Unaccepted files:", files);
// Ici, vous pourriez afficher une notification
};
const handleDeleteFile = (id: string) => {
setFiles(prev => prev.filter(f => f.id !== id));
};
const handleRetryFile = (id: string) => {
const file = files.find(f => f.id === id);
if (!file) return;
simulateUploadFile( progress => {
setFiles(prev => prev.map(f => (f.id === id ? { ...f, progress, failed: false } : f)));
});
};
return (
<FileUpload
variant="progress-bar"
files={files}
accept="image/*"
hint="Please upload PNG or JPEG images only."
onFilesAdded={handleFilesAdded}
onDeleteFile={handleDeleteFile}
onRetryFile={handleRetryFile}
/>
);File size limits
Max size: 1MB - Files exceeding this limit will be rejected
Upload files (max. 1 MB).
const [files, setFiles] = useState<UploadedFileItemProps[]>([]);
const MAX_SIZE = 1024 * 1024 * 1; // 1MB
const handleFilesAdded = (newFiles: File[]) => {
const newFilesWithIds = newFiles.map(file => ({
id: Math.random().toString(),
name: file.name,
size: file.size,
type: file.type as FileType,
progress: 0,
}));
setFiles([...newFilesWithIds, ...files]);
newFiles.forEach((file, index) => {
const fileObject = newFiles.find(
f =>
f.name === newFilesWithIds.find(nf => nf.id === newFilesWithIds[index].id)?.name
);
if (fileObject) {
simulateUploadFile( progress => {
setFiles(prev =>
prev.map(f => (f.id === newFilesWithIds[index].id ? { ...f, progress } : f))
);
});
}
});
};
const handleSizeLimitExceed = (files: FileList) => {
console.log("Files too large:", files);
// Ici, vous pourriez afficher une notification
};
const handleDeleteFile = (id: string) => {
setFiles(prev => prev.filter(f => f.id !== id));
};
const handleRetryFile = (id: string) => {
const file = files.find(f => f.id === id);
if (!file) return;
simulateUploadFile( progress => {
setFiles(prev => prev.map(f => (f.id === id ? { ...f, progress, failed: false } : f)));
});
};
return (
<FileUpload
variant="progress-bar"
files={files}
maxSize={MAX_SIZE}
hint={"Upload files (max. 1 MB)."}
onFilesAdded={handleFilesAdded}
onDeleteFile={handleDeleteFile}
onRetryFile={handleRetryFile}
/>
);Single file upload
allowsMultiple: false - Only one file can be selected at a time
Select a single file to upload.
const [files, setFiles] = useState<UploadedFileItemProps[]>([]);
const handleFilesAdded = (newFiles: File[]) => {
const newFilesWithIds = newFiles.map(file => ({
id: Math.random().toString(),
name: file.name,
size: file.size,
type: file.type as FileType,
progress: 0,
}));
setFiles(newFilesWithIds); // Remplace le fichier existant
newFiles.forEach((file, index) => {
const fileObject = newFiles.find(
f =>
f.name === newFilesWithIds.find(nf => nf.id === newFilesWithIds[index].id)?.name
);
if (fileObject) {
simulateUploadFile( progress => {
setFiles(prev =>
prev.map(f => (f.id === newFilesWithIds[index].id ? { ...f, progress } : f))
);
});
}
});
};
const handleDeleteFile = (id: string) => {
setFiles(prev => prev.filter(f => f.id !== id));
};
const handleRetryFile = (id: string) => {
const file = files.find(f => f.id === id);
if (!file) return;
simulateUploadFile( progress => {
setFiles(prev => prev.map(f => (f.id === id ? { ...f, progress, failed: false } : f)));
});
};
return (
<FileUpload
variant="progress-bar"
files={files}
allowsMultiple={false}
hint="Select a single file to upload."
onFilesAdded={handleFilesAdded}
onDeleteFile={handleDeleteFile}
onRetryFile={handleRetryFile}
/>
);Disabled state
File upload is currently disabled.
<FileUpload
variant="progress-bar"
files={files}
isDisabled={true}
hint="File upload is currently disabled."
onFilesAdded={handleFilesAdded}
onDeleteFile={handleDeleteFile}
onRetryFile={handleRetryFile}
/>API Reference
FileUpload
| Props | Type | Default | Description |
|---|---|---|---|
| files | UploadedFileItemProps[] | - | **Required.** An array of `UploadedFileItemProps` objects to display the uploaded files. |
| variant? | "progress-bar" | "progress-fill" | "progress-bar" | The progress display variant. `progress-bar` shows a progress bar below, while `progress-fill` uses a background fill effect. |
| accept? | string | - | Accepted file types (e.g., "image/*", ".pdf,.docx"). |
| maxSize? | number | - | Maximum file size in bytes. Larger files will be rejected. |
| allowsMultiple? | boolean | true | Allows multiple file selection. |
| hint? | string | - | Hint text displayed in the drop zone. |
| isDisabled? | boolean | false | Disables the upload functionality. |
| onFilesAdded? | (files: File[]) => void | - | Called when files are added (dropped or selected). |
| onDeleteFile? | (fileId: string) => void | - | Called when a file is deleted. |
| onRetryFile? | (fileId: string) => void | - | Called when file upload is retried. |
UploadedFileItemProps
| Props | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique identifier for the file |
| name | string | - | File name |
| size | number | - | File size in bytes |
| progress | number | - | Upload progress (0-100) |
| failed? | boolean | false | Whether the upload failed |
| type? | string | - | File type for icon display |
| url? | string | - | The final URL of the file after upload. |
FileUploadForm
This component inherits all UI props from the base `FileUpload` component (e.g., `accept`, `maxSize`, `variant`, etc.) and adds the following for RHF integration and upload logic.
| Prop | Type | Default | Description |
|---|---|---|---|
| control | Control<TFieldValues> | - | **Required.** The `control` object from `react-hook-form`. |
| name | FieldPath<TFieldValues> | - | **Required.** The field name in the form schema (e.g., "documents"). |
| label | string | - | Label displayed above the input field. |
| storageMode? | 'files' | 'metadata' | 'files' | Defines the RHF field value format. **'files'** stores raw `File[]`. **'metadata'** stores `UploadedFileItemProps[]` (with URL/ID after upload). |
| uploadFn? | (file, onProgress) => Promise<TResult> | - | Asynchronous function to upload the file to a third-party service. |
| transformFiles? | (files) => UploadedFileItemProps[] | - | Function to map raw `File` objects to the `UploadedFileItemProps` interface before adding them to the component state. |
| onUploadComplete? | (fileId, result) => void | - | Called after a successful upload, with the file ID and the result from `uploadFn`. |
| onFileDeleted? | (file: UploadedFileItemProps) => void | - | Callback triggered when a file is successfully deleted from the list. |
Custom Components with FileUploadCustom
For advanced customization:
FileUploadCustom.Root- Container wrapperFileUploadCustom.DropZone- File drop and selection areaFileUploadCustom.List- Container for file list itemsFileUploadCustom.ListItemProgressBar- File item with progress barFileUploadCustom.ListItemProgressFill- File item with fill progress
Utilities
| Function | Signature | Description |
|---|---|---|
| getReadableFileSize | (bytes: number) => string | Converts bytes to human-readable format (KB, MB, etc.) |