Multi-image
drag & drop images here, or click to select
Up to 6 files, Max size: 1 MB
Installation
- CLI
- Manual
- pnpm
- npm
- yarn
- bun
bash
pnpm dlx shadcn@latest add https://edgestore.dev/r/image-uploader.json
bash
npx shadcn@latest add https://edgestore.dev/r/image-uploader.json
bash
npx shadcn@latest add https://edgestore.dev/r/image-uploader.json
shell
bunx --bun shadcn@latest add https://edgestore.dev/r/image-uploader.json
Setup for manual installation
First you will need to follow the manual install setup guide.
Install required components
Copy this component
tsx
'use client';import { cn } from '@/lib/utils';import { Trash2Icon, XIcon } from 'lucide-react';import * as React from 'react';import { type DropzoneOptions } from 'react-dropzone';import { Dropzone } from './dropzone';import { ProgressCircle } from './progress-circle';import { useUploader } from './uploader-provider';/*** Props for the ImageList component.** @interface ImageListProps* @extends {React.HTMLAttributes<HTMLDivElement>}*/export interface ImageListProps extends React.HTMLAttributes<HTMLDivElement> {/*** Whether the image deletion controls should be disabled.*/disabled?: boolean;}/*** Displays a grid of image previews with upload status and controls.** @component* @example* ```tsx* <ImageList className="my-4" />* ```*/const ImageList = React.forwardRef<HTMLDivElement, ImageListProps>(({ className, disabled: initialDisabled, ...props }, ref) => {const { fileStates, removeFile, cancelUpload } = useUploader();// Create temporary URLs for image previewsconst tempUrls = React.useMemo(() => {const urls: Record<string, string> = {};fileStates.forEach((fileState) => {if (fileState.file) {urls[fileState.key] = URL.createObjectURL(fileState.file);}});return urls;}, [fileStates]);// Clean up temporary URLs on unmountReact.useEffect(() => {return () => {Object.values(tempUrls).forEach((url) => {URL.revokeObjectURL(url);});};}, [tempUrls]);if (!fileStates.length) return null;return (<divref={ref}className={cn('mt-4 grid grid-cols-3 gap-2', className)}{...props}>{fileStates.map((fileState) => {const displayUrl = tempUrls[fileState.key] ?? fileState.url;return (<divkey={fileState.key}className={'relative aspect-square h-full w-full rounded-md border-0 bg-gray-100 p-0 shadow-md dark:bg-gray-800'}>{displayUrl ? (<imgclassName="h-full w-full rounded-md object-cover"src={displayUrl}alt={fileState.file.name}/>) : (<div className="flex h-full w-full items-center justify-center bg-gray-200 dark:bg-gray-700"><span className="text-xs text-gray-500 dark:text-gray-400">No Preview</span></div>)}{/* Upload progress indicator */}{fileState.status === 'UPLOADING' && (<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-md bg-black/70"><ProgressCircle progress={fileState.progress} /></div>)}{/* Delete/cancel button */}{displayUrl && !initialDisabled && (<buttontype="button"className="group pointer-events-auto absolute right-1 top-1 z-10 -translate-y-1/4 translate-x-1/4 transform rounded-full border border-gray-400 bg-white p-1 shadow-md transition-all hover:scale-110 dark:border-gray-600 dark:bg-gray-800"onClick={(e) => {e.stopPropagation();if (fileState.status === 'UPLOADING') {cancelUpload(fileState.key);} else {removeFile(fileState.key);}}}>{fileState.status === 'UPLOADING' ? (<XIcon className="block h-4 w-4 text-gray-500 dark:text-gray-400" />) : (<Trash2Icon className="block h-4 w-4 text-gray-500 dark:text-gray-400" />)}</button>)}</div>);})}</div>);},);ImageList.displayName = 'ImageList';/*** Props for the ImageDropzone component.** @interface ImageDropzoneProps* @extends {React.HTMLAttributes<HTMLDivElement>}*/export interface ImageDropzonePropsextends React.HTMLAttributes<HTMLDivElement> {/*** Whether the dropzone is disabled.*/disabled?: boolean;/*** Options passed to the underlying Dropzone component.* Cannot include 'disabled' or 'onDrop' as they are handled internally.*/dropzoneOptions?: Omit<DropzoneOptions, 'disabled' | 'onDrop'>;/*** Ref for the input element inside the Dropzone.*/inputRef?: React.Ref<HTMLInputElement>;}/*** A dropzone component specifically for image uploads.** @component* @example* ```tsx* <ImageDropzone* dropzoneOptions={{* maxFiles: 5,* maxSize: 1024 * 1024 * 2, // 2MB* }}* />* ```*/const ImageDropzone = React.forwardRef<HTMLDivElement, ImageDropzoneProps>(({ dropzoneOptions, className, disabled, inputRef, ...props }, ref) => {return (<div ref={ref} className={className} {...props}><Dropzoneref={inputRef}dropzoneOptions={{accept: { 'image/*': [] },...dropzoneOptions,}}disabled={disabled}dropMessageActive="Drop images here..."dropMessageDefault="drag & drop images here, or click to select"/></div>);},);ImageDropzone.displayName = 'ImageDropzone';/*** Props for the ImageUploader component.** @interface ImageUploaderProps* @extends {React.HTMLAttributes<HTMLDivElement>}*/export interface ImageUploaderPropsextends React.HTMLAttributes<HTMLDivElement> {/*** Maximum number of images allowed.*/maxFiles?: number;/*** Maximum file size in bytes.*/maxSize?: number;/*** Whether the uploader is disabled.*/disabled?: boolean;/*** Additional className for the dropzone component.*/dropzoneClassName?: string;/*** Additional className for the image list component.*/imageListClassName?: string;/*** Ref for the input element inside the Dropzone.*/inputRef?: React.Ref<HTMLInputElement>;}/*** A complete image uploader component with dropzone and image grid preview.** @component* @example* ```tsx* <ImageUploader* maxFiles={10}* maxSize={1024 * 1024 * 5} // 5MB* />* ```*/const ImageUploader = React.forwardRef<HTMLDivElement, ImageUploaderProps>(({maxFiles,maxSize,disabled,className,dropzoneClassName,imageListClassName,inputRef,...props},ref,) => {return (<div ref={ref} className={cn('w-full space-y-4', className)} {...props}><ImageDropzoneref={inputRef}dropzoneOptions={{maxFiles,maxSize,}}disabled={disabled}className={dropzoneClassName}/><ImageList className={imageListClassName} disabled={disabled} /></div>);},);ImageUploader.displayName = 'ImageUploader';export { ImageList, ImageDropzone, ImageUploader };
Usage
tsx
'use client';import { ImageUploader } from '@/components/upload/multi-image';import {UploaderProvider,type UploadFn,} from '@/components/upload/uploader-provider';import { useEdgeStore } from '@/lib/edgestore';import * as React from 'react';export function MultiImageDropzoneUsage() {const { edgestore } = useEdgeStore();const uploadFn: UploadFn = React.useCallback(async ({ file, onProgressChange, signal }) => {const res = await edgestore.publicImages.upload({file,signal,onProgressChange,});// you can run some server action or api here// to add the necessary data to your databaseconsole.log(res);return res;},[edgestore],);return (<UploaderProvider uploadFn={uploadFn} autoUpload><ImageUploadermaxFiles={10}maxSize={1024 * 1024 * 1} // 1 MB/></UploaderProvider>);}