Upload Dropzone with Progress
A dropzone that uploads multiple files. It shows the progress of each file upload.
Demo
invoice_123.pdf
2 MB
Completed
photo.jpeg
12 MB
Installation
npx shadcn@latest add "https://better-upload.com/r/upload-dropzone-progress.json"
Install the following dependencies:
npm install lucide-react react-dropzone
Make sure to have shadcn/ui set up in your project, with the progress component installed.
Copy and paste the following code into your project.
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
import type { UploadHookControl } from 'better-upload/client';
import { formatBytes } from 'better-upload/client/helpers';
import { Dot, File, Upload } from 'lucide-react';
import { useId } from 'react';
import { useDropzone } from 'react-dropzone';
type UploadDropzoneProgressProps = {
control: UploadHookControl<true>;
accept?: string;
metadata?: Record<string, unknown>;
description?:
| {
fileTypes?: string;
maxFileSize?: string;
maxFiles?: number;
}
| string;
uploadOverride?: (
...args: Parameters<UploadHookControl<true>['upload']>
) => void;
// Add any additional props you need.
};
export function UploadDropzoneProgress({
control: { upload, isPending, progresses },
accept,
metadata,
description,
uploadOverride,
}: UploadDropzoneProgressProps) {
const id = useId();
const { getRootProps, getInputProps, isDragActive, inputRef } = useDropzone({
onDrop: (files) => {
if (files.length > 0) {
if (uploadOverride) {
uploadOverride(files, { metadata });
} else {
upload(files, { metadata });
}
}
inputRef.current.value = '';
},
noClick: true,
});
return (
<div className="flex flex-col gap-3">
<div
className={cn(
'relative rounded-lg border border-dashed transition-colors',
{
'border-primary/70': isDragActive,
}
)}
>
<label
{...getRootProps()}
className={cn(
'dark:bg-input/10 flex w-full min-w-72 cursor-pointer flex-col items-center justify-center rounded-lg bg-transparent px-2 py-6 transition-colors',
{
'text-muted-foreground cursor-not-allowed': isPending,
'hover:bg-accent dark:hover:bg-accent/30': !isPending,
}
)}
htmlFor={id}
>
<div className="my-2">
<Upload className="size-6" />
</div>
<div className="mt-3 space-y-1 text-center">
<p className="text-sm font-semibold">Drag and drop files here</p>
<p className="text-muted-foreground max-w-64 text-xs">
{typeof description === 'string' ? (
description
) : (
<>
{description?.maxFiles &&
`You can upload ${description.maxFiles} file${description.maxFiles !== 1 ? 's' : ''}.`}{' '}
{description?.maxFileSize &&
`${description.maxFiles !== 1 ? 'Each u' : 'U'}p to ${description.maxFileSize}.`}{' '}
{description?.fileTypes &&
`Accepted ${description.fileTypes}.`}
</>
)}
</p>
</div>
<input
{...getInputProps()}
type="file"
multiple
id={id}
accept={accept}
disabled={isPending}
/>
</label>
{isDragActive && (
<div className="bg-background pointer-events-none absolute inset-0 rounded-lg">
<div className="dark:bg-accent/30 bg-accent flex size-full flex-col items-center justify-center rounded-lg">
<div className="my-2">
<Upload className="size-6" />
</div>
<p className="mt-3 text-sm font-semibold">Drop files here</p>
</div>
</div>
)}
</div>
<div className="grid gap-2">
{progresses.map((progress) => (
<div
key={progress.objectKey}
className={cn(
'dark:bg-input/10 flex items-center gap-2 rounded-lg border bg-transparent p-3',
{
'bg-red-500/[0.04]! border-red-500/60':
progress.status === 'failed',
}
)}
>
<FileIcon type={progress.type} />
<div className="grid grow gap-1">
<div className="flex items-center gap-0.5">
<p className="max-w-40 truncate text-sm font-medium">
{progress.name}
</p>
<Dot className="text-muted-foreground size-4" />
<p className="text-muted-foreground text-xs">
{formatBytes(progress.size)}
</p>
</div>
<div className="flex h-4 items-center">
{progress.progress < 1 && progress.status !== 'failed' ? (
<Progress className="h-1.5" value={progress.progress * 100} />
) : progress.status === 'failed' ? (
<p className="text-xs text-red-500">Failed</p>
) : (
<p className="text-muted-foreground text-xs">Completed</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
);
}
const iconCaptions = {
'image/': 'IMG',
'video/': 'VID',
'audio/': 'AUD',
'application/pdf': 'PDF',
'application/zip': 'ZIP',
'application/x-rar-compressed': 'RAR',
'application/x-7z-compressed': '7Z',
'application/x-tar': 'TAR',
'application/json': 'JSON',
'application/javascript': 'JS',
'text/plain': 'TXT',
'text/csv': 'CSV',
'text/html': 'HTML',
'text/css': 'CSS',
'application/xml': 'XML',
'application/x-sh': 'SH',
'application/x-python-code': 'PY',
'application/x-executable': 'EXE',
'application/x-disk-image': 'ISO',
};
function FileIcon({ type }: { type: string }) {
const caption = Object.entries(iconCaptions).find(([key]) =>
type.startsWith(key)
)?.[1];
return (
<div className="relative shrink-0">
<File className="text-muted-foreground size-12" strokeWidth={1} />
{caption && (
<span className="bg-primary text-primary-foreground absolute bottom-2.5 left-0.5 select-none rounded px-1 py-px text-xs font-semibold">
{caption}
</span>
)}
</div>
);
}
Update the import paths to match your project setup.
Usage
The <UploadDropzoneProgress />
should be used with the useUploadFiles
hook.
'use client';
import { useUploadFiles } from 'better-upload/client';
import { UploadDropzoneProgress } from '@/components/ui/upload-dropzone-progress';
export function Uploader() {
const { control } = useUploadFiles({
route: 'demo',
});
return <UploadDropzoneProgress control={control} accept="image/*" />;
}
When clicked, the dropzone will open a file picker dialog. When selected or dropped, the files will be uploaded to the desired route.
Description
You can customize the description shown in the dropzone. You can pass a string, or an object with the following properties:
maxFiles
: The maximum number of files that can be uploaded.maxFileSize
: The maximum size of the files that can be uploaded, use a formatted string (e.g.10MB
).fileTypes
: The file types that can be uploaded.
<UploadDropzone
control={control}
accept="image/*"
description={{
maxFiles: 4,
maxFileSize: '2MB',
fileTypes: 'JPEG, PNG, GIF',
}}
/>
Note that this is only cosmetic and does not enforce any restrictions client-side.
Props
Prop | Type | Default |
---|---|---|
control | object | - |
accept? | string | undefined | - |
description? | string | object | undefined | - |
metadata? | Record<string, unknown> | undefined | - |
uploadOverride? | function | undefined | - |