Kaapi UI
← Back to Design System

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

or drag and drop

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

or drag and drop

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

or drag and drop

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

or drag and drop

PNG, JPG or JPEG (max. 5MB)

or drag and drop

Upload your documents (PDF, DOC, DOCX)

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

or drag and drop

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

or drag and drop

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

or drag and drop

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

or drag and drop

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

PropsTypeDefaultDescription
filesUploadedFileItemProps[]-**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?booleantrueAllows multiple file selection.
hint?string-Hint text displayed in the drop zone.
isDisabled?booleanfalseDisables 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

PropsTypeDefaultDescription
idstring-Unique identifier for the file
namestring-File name
sizenumber-File size in bytes
progressnumber-Upload progress (0-100)
failed?booleanfalseWhether 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.

PropTypeDefaultDescription
controlControl<TFieldValues>-**Required.** The `control` object from `react-hook-form`.
nameFieldPath<TFieldValues>-**Required.** The field name in the form schema (e.g., "documents").
labelstring-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 wrapper
  • FileUploadCustom.DropZone- File drop and selection area
  • FileUploadCustom.List - Container for file list items
  • FileUploadCustom.ListItemProgressBar - File item with progress bar
  • FileUploadCustom.ListItemProgressFill - File item with fill progress

Utilities

FunctionSignatureDescription
getReadableFileSize(bytes: number) => stringConverts bytes to human-readable format (KB, MB, etc.)