<FileUpload.Root>
<FileUpload.Label>Attachments</FileUpload.Label>
<FileUpload.Dropzone name="attachments" multiple description="SVG, PNG, JPG or GIF, up to 10 MB" />
<FileUpload.ErrorMessage>Upload at least one file.</FileUpload.ErrorMessage>
</FileUpload.Root>Features
- Accepts files from the file picker or drag and drop.
- Passes selected files to
onChange; your app uploads them. - Supports single or multiple files and a file type filter for the picker.
- Lists files as in progress, completed, or failed.
- Supports cancel, remove, and retry actions on each file.
- Supports custom dropzone titles, icon, button, and description.
Anatomy
<FileUpload.Root>
<FileUpload.Label>Attachments</FileUpload.Label>
<FileUpload.Dropzone name="attachments" />
<FileUpload.ErrorMessage>Upload at least one file.</FileUpload.ErrorMessage>
<FileUpload.List>
<FileUpload.Item fileName="report.pdf" />
<FileUpload.Item fileName="photo.png" />
</FileUpload.List>
</FileUpload.Root>Dropzone Content
Customize the dropzone with title, dragOverTitle, buttonLabel, buttonProps, and description. Set accept to filter the file picker, and startSlot={null} to hide the upload icon.
import { FileUpload, Stack } from "@vortexlabs/vortex";
<Stack gap={6}>
<FileUpload.Root>
<FileUpload.Dropzone
name="avatar"
title="Drop your avatar here"
dragOverTitle="Release to upload"
buttonLabel="Browse"
buttonProps={{ variant: "primary" }}
description="PNG or JPG, up to 2 MB"
accept="image/png,image/jpeg"
/>
</FileUpload.Root>
<FileUpload.Root>
<FileUpload.Dropzone name="minimal" startSlot={null} />
</FileUpload.Root>
</Stack>File Items
Set status to in-progress (shows progress), completed, or error. Actions appear only with their callbacks: onCancel while uploading, onRemove when completed or failed, and onRetry for retryable errors.
- Brand guidelines.pdf2.5 MB50%
- Quarterly report.pdf1.8 MB · Uploaded
- Team photo.pngUpload failed. Check your connection.
- presentation.keyUnsupported file format.
<FileUpload.List>
<FileUpload.Item
fileName="Brand guidelines.pdf"
status="in-progress"
description="2.5 MB"
progress={50}
onCancel={handleCancel}
/>
<FileUpload.Item
fileName="Quarterly report.pdf"
description="1.8 MB"
statusText="Uploaded"
onRemove={handleRemove}
/>
<FileUpload.Item
fileName="Team photo.png"
status="error"
errorMessage="Upload failed. Check your connection."
onRetry={handleRetry}
onRemove={handleRemove}
/>
<FileUpload.Item
fileName="presentation.key"
status="error"
errorMessage="Unsupported file format."
onRemove={handleRemove}
/>
</FileUpload.List>Disabled and Error States
Set error on FileUpload.Root to show FileUpload.ErrorMessage. Set disabled to block picking and dropping files; item actions stay available.
<Stack gap={6}>
<FileUpload.Root error>
<FileUpload.Label>Attachments</FileUpload.Label>
<FileUpload.Dropzone name="error-files" description="SVG, PNG, JPG or GIF, up to 10 MB" />
<FileUpload.ErrorMessage>Upload at least one file.</FileUpload.ErrorMessage>
</FileUpload.Root>
<FileUpload.Root disabled>
<FileUpload.Label>Attachments</FileUpload.Label>
<FileUpload.Dropzone name="disabled-files" description="SVG, PNG, JPG or GIF, up to 10 MB" />
</FileUpload.Root>
</Stack>Uploading Files
onChange receives the picked or dropped files. File Upload doesn't upload them or submit them with a native form. Without multiple, only the first dropped file is passed. Validate each file, upload it, and render a FileUpload.Item with its current status.
"use client";
import { FileUpload } from "@vortexlabs/vortex";
import React from "react";
type Upload = {
id: string;
file: File;
progress: number;
status: "in-progress" | "completed" | "error";
errorMessage?: string;
};
const ACCEPTED_TYPES = ["image/png", "image/jpeg"];
const MAX_SIZE = 10 * 1024 * 1024;
const formatSize = (bytes: number) => {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
};
export const FileUploadLifecycleExample = () => {
const [uploads, setUploads] = React.useState<Upload[]>([]);
const timersRef = React.useRef(new Map<string, ReturnType<typeof setInterval>>());
React.useEffect(() => {
const timers = timersRef.current;
return () => {
for (const timer of timers.values()) clearInterval(timer);
};
}, []);
const update = (id: string, patch: Partial<Upload>) => {
setUploads((prev) => prev.map((upload) => (upload.id === id ? { ...upload, ...patch } : upload)));
};
// Simulated progress; replace with your upload request
const startUpload = (id: string) => {
let progress = 0;
const timer = setInterval(() => {
progress = Math.min(progress + 10, 100);
if (progress < 100) {
update(id, { progress });
return;
}
clearInterval(timer);
timersRef.current.delete(id);
update(id, { progress, status: "completed" });
}, 200);
timersRef.current.set(id, timer);
};
const remove = (id: string) => {
clearInterval(timersRef.current.get(id));
timersRef.current.delete(id);
setUploads((prev) => prev.filter((upload) => upload.id !== id));
};
const handleChange = ({ value }: { value: File[] }) => {
const next = value.map<Upload>((file) => {
const id = crypto.randomUUID();
if (!ACCEPTED_TYPES.includes(file.type)) {
return { id, file, progress: 0, status: "error", errorMessage: "Unsupported file format." };
}
if (file.size > MAX_SIZE) {
return { id, file, progress: 0, status: "error", errorMessage: "File is larger than 10 MB." };
}
return { id, file, progress: 0, status: "in-progress" };
});
setUploads((prev) => [...prev, ...next]);
for (const upload of next) {
if (upload.status === "in-progress") startUpload(upload.id);
}
};
return (
<FileUpload.Root>
<FileUpload.Label>Photos</FileUpload.Label>
<FileUpload.Dropzone
name="photos"
multiple
accept={ACCEPTED_TYPES.join(",")}
description="PNG or JPG, up to 10 MB"
onChange={handleChange}
/>
<FileUpload.List>
{uploads.map((upload) => (
<FileUpload.Item
key={upload.id}
fileName={upload.file.name}
status={upload.status}
description={formatSize(upload.file.size)}
statusText="Uploaded"
progress={upload.progress}
errorMessage={upload.errorMessage}
onCancel={() => remove(upload.id)}
onRemove={() => remove(upload.id)}
/>
))}
</FileUpload.List>
</FileUpload.Root>
);
};Accessibility
FileUpload.Label; without it, the input is named only by its button.cancelAriaLabel, removeAriaLabel, and retryLabel; the defaults are English.required doesn't check them.