Better Upload

Upload Dropzone

A dropzone that uploads multiple files.

Demo

Installation

npx shadcn@latest add "https://better-upload.com/r/upload-dropzone.json"

Install the following dependencies:

npm install lucide-react react-dropzone

Make sure to have shadcn/ui set up in your project.

Copy and paste the following code into your project.

import { cn } from '@/lib/utils';
import type { UploadHookControl } from 'better-upload/client';
import { Loader2, Upload } from 'lucide-react';
import { useId } from 'react';
import { useDropzone } from 'react-dropzone';

type UploadDropzoneProps = {
  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 UploadDropzone({
  control: { upload, isPending },
  accept,
  metadata,
  description,
  uploadOverride,
}: UploadDropzoneProps) {
  const id = useId();

  const { getRootProps, getInputProps, isDragActive, inputRef } = useDropzone({
    onDrop: (files) => {
      if (files.length > 0 && !isPending) {
        if (uploadOverride) {
          uploadOverride(files, { metadata });
        } else {
          upload(files, { metadata });
        }
      }
      inputRef.current.value = '';
    },
    noClick: true,
  });

  return (
    <div
      className={cn(
        'border-input relative rounded-lg border border-dashed transition-colors',
        {
          'border-primary/80': 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">
          {isPending ? (
            <Loader2 className="size-6 animate-spin" />
          ) : (
            <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>
  );
}

Update the import paths to match your project setup.

Usage

The <UploadDropzone /> should be used with the useUploadFiles hook.

'use client';

import { useUploadFiles } from 'better-upload/client';
import { UploadDropzone } from '@/components/ui/upload-dropzone';

export function Uploader() {
  const { control } = useUploadFiles({
    route: 'demo',
  });

  return <UploadDropzone 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

PropTypeDefault
control
object
-
accept?
string | undefined
-
description?
string | object | undefined
-
metadata?
Record<string, unknown> | undefined
-
uploadOverride?
function | undefined
-