Dropzone
Drag-and-drop file upload component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring file validation, preview, and customizable upload interfaces.
Powered by
'use client';import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/ui/shadcn-io/dropzone';import { useState } from 'react';const Example = () => { const [files, setFiles] = useState<File[] | undefined>(); const handleDrop = (files: File[]) => { console.log(files); setFiles(files); }; return ( <Dropzone accept={{ 'image/*': [] }} maxFiles={10} maxSize={1024 * 1024 * 10} minSize={1024} onDrop={handleDrop} onError={console.error} src={files} > <DropzoneEmptyState /> <DropzoneContent /> </Dropzone> );};export default Example;
'use client';import { UploadIcon } from 'lucide-react';import type { ReactNode } from 'react';import { createContext, useContext } from 'react';import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone';import { useDropzone } from 'react-dropzone';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions['accept']; maxSize?: DropzoneOptions['maxSize']; minSize?: DropzoneOptions['minSize']; maxFiles?: DropzoneOptions['maxFiles'];};const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`;};const DropzoneContext = createContext<DropzoneContextType | undefined>( undefined);export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void; children?: ReactNode;};export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props}: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( <DropzoneContext.Provider key={JSON.stringify(src)} value={{ src, accept, maxSize, minSize, maxFiles }} > <Button className={cn( 'relative h-auto w-full flex-col overflow-hidden p-8', isDragActive && 'outline-none ring-1 ring-ring', className )} disabled={disabled} type="button" variant="outline" {...getRootProps()} > <input {...getInputProps()} disabled={disabled} /> {children} </Button> </DropzoneContext.Provider> );};const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); } return context;};export type DropzoneContentProps = { children?: ReactNode; className?: string;};const maxLabelItems = 3;export const DropzoneContent = ({ children, className,}: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate font-medium text-sm"> {src.length > maxLabelItems ? `${new Intl.ListFormat('en').format( src.slice(0, maxLabelItems).map((file) => file.name) )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat('en').format(src.map((file) => file.name))} </p> <p className="w-full text-wrap text-muted-foreground text-xs"> Drag and drop or click to replace </p> </div> );};export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string;};export const DropzoneEmptyState = ({ children, className,}: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ''; if (accept) { caption += 'Accepts '; caption += new Intl.ListFormat('en').format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate text-wrap font-medium text-sm"> Upload {maxFiles === 1 ? 'a file' : 'files'} </p> <p className="w-full truncate text-wrap text-muted-foreground text-xs"> Drag and drop or click to upload </p> {caption && ( <p className="text-wrap text-muted-foreground text-xs">{caption}.</p> )} </div> );};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/dropzone.json
npx shadcn@latest add https://www.shadcn.io/registry/dropzone.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/dropzone.json
bunx shadcn@latest add https://www.shadcn.io/registry/dropzone.json
Features
- Drag-and-drop uploads - File selection with visual feedback using react-dropzone for React apps
- File validation - Type, size, and count restrictions with error handling using TypeScript
- Image previews - Thumbnail display for uploaded images with responsive design using Tailwind CSS
- Custom states - Empty state, loading, error, and success messaging using shadcn/ui components
- Multiple files - Single or multi-file uploads with replacement functionality using JavaScript
- Context API - Shared dropzone state across components for Next.js applications
- Open source - Free file upload component with full customization
Examples
With min and max sizes
'use client';import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/ui/shadcn-io/dropzone';import { useState } from 'react';const Example = () => { const [files, setFiles] = useState<File[] | undefined>(); const handleDrop = (files: File[]) => { console.log(files); setFiles(files); }; return ( <Dropzone maxSize={1024 * 1024 * 10} minSize={1024} onDrop={handleDrop} onError={console.error} src={files} > <DropzoneEmptyState /> <DropzoneContent /> </Dropzone> );};export default Example;
'use client';import { UploadIcon } from 'lucide-react';import type { ReactNode } from 'react';import { createContext, useContext } from 'react';import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone';import { useDropzone } from 'react-dropzone';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions['accept']; maxSize?: DropzoneOptions['maxSize']; minSize?: DropzoneOptions['minSize']; maxFiles?: DropzoneOptions['maxFiles'];};const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`;};const DropzoneContext = createContext<DropzoneContextType | undefined>( undefined);export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void; children?: ReactNode;};export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props}: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( <DropzoneContext.Provider key={JSON.stringify(src)} value={{ src, accept, maxSize, minSize, maxFiles }} > <Button className={cn( 'relative h-auto w-full flex-col overflow-hidden p-8', isDragActive && 'outline-none ring-1 ring-ring', className )} disabled={disabled} type="button" variant="outline" {...getRootProps()} > <input {...getInputProps()} disabled={disabled} /> {children} </Button> </DropzoneContext.Provider> );};const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); } return context;};export type DropzoneContentProps = { children?: ReactNode; className?: string;};const maxLabelItems = 3;export const DropzoneContent = ({ children, className,}: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate font-medium text-sm"> {src.length > maxLabelItems ? `${new Intl.ListFormat('en').format( src.slice(0, maxLabelItems).map((file) => file.name) )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat('en').format(src.map((file) => file.name))} </p> <p className="w-full text-wrap text-muted-foreground text-xs"> Drag and drop or click to replace </p> </div> );};export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string;};export const DropzoneEmptyState = ({ children, className,}: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ''; if (accept) { caption += 'Accepts '; caption += new Intl.ListFormat('en').format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate text-wrap font-medium text-sm"> Upload {maxFiles === 1 ? 'a file' : 'files'} </p> <p className="w-full truncate text-wrap text-muted-foreground text-xs"> Drag and drop or click to upload </p> {caption && ( <p className="text-wrap text-muted-foreground text-xs">{caption}.</p> )} </div> );};
Multiple files
'use client';import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/ui/shadcn-io/dropzone';import { useState } from 'react';const Example = () => { const [files, setFiles] = useState<File[] | undefined>(); const handleDrop = (files: File[]) => { console.log(files); setFiles(files); }; return ( <Dropzone maxFiles={3} onDrop={handleDrop} onError={console.error} src={files} > <DropzoneEmptyState /> <DropzoneContent /> </Dropzone> );};export default Example;
'use client';import { UploadIcon } from 'lucide-react';import type { ReactNode } from 'react';import { createContext, useContext } from 'react';import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone';import { useDropzone } from 'react-dropzone';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions['accept']; maxSize?: DropzoneOptions['maxSize']; minSize?: DropzoneOptions['minSize']; maxFiles?: DropzoneOptions['maxFiles'];};const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`;};const DropzoneContext = createContext<DropzoneContextType | undefined>( undefined);export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void; children?: ReactNode;};export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props}: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( <DropzoneContext.Provider key={JSON.stringify(src)} value={{ src, accept, maxSize, minSize, maxFiles }} > <Button className={cn( 'relative h-auto w-full flex-col overflow-hidden p-8', isDragActive && 'outline-none ring-1 ring-ring', className )} disabled={disabled} type="button" variant="outline" {...getRootProps()} > <input {...getInputProps()} disabled={disabled} /> {children} </Button> </DropzoneContext.Provider> );};const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); } return context;};export type DropzoneContentProps = { children?: ReactNode; className?: string;};const maxLabelItems = 3;export const DropzoneContent = ({ children, className,}: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate font-medium text-sm"> {src.length > maxLabelItems ? `${new Intl.ListFormat('en').format( src.slice(0, maxLabelItems).map((file) => file.name) )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat('en').format(src.map((file) => file.name))} </p> <p className="w-full text-wrap text-muted-foreground text-xs"> Drag and drop or click to replace </p> </div> );};export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string;};export const DropzoneEmptyState = ({ children, className,}: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ''; if (accept) { caption += 'Accepts '; caption += new Intl.ListFormat('en').format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate text-wrap font-medium text-sm"> Upload {maxFiles === 1 ? 'a file' : 'files'} </p> <p className="w-full truncate text-wrap text-muted-foreground text-xs"> Drag and drop or click to upload </p> {caption && ( <p className="text-wrap text-muted-foreground text-xs">{caption}.</p> )} </div> );};
Images only
'use client';import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/ui/shadcn-io/dropzone';import { useState } from 'react';const Example = () => { const [files, setFiles] = useState<File[] | undefined>(); const handleDrop = (files: File[]) => { console.log(files); setFiles(files); }; return ( <Dropzone accept={{ 'image/*': [] }} onDrop={handleDrop} onError={console.error} src={files} > <DropzoneEmptyState /> <DropzoneContent /> </Dropzone> );};export default Example;
'use client';import { UploadIcon } from 'lucide-react';import type { ReactNode } from 'react';import { createContext, useContext } from 'react';import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone';import { useDropzone } from 'react-dropzone';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions['accept']; maxSize?: DropzoneOptions['maxSize']; minSize?: DropzoneOptions['minSize']; maxFiles?: DropzoneOptions['maxFiles'];};const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`;};const DropzoneContext = createContext<DropzoneContextType | undefined>( undefined);export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void; children?: ReactNode;};export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props}: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( <DropzoneContext.Provider key={JSON.stringify(src)} value={{ src, accept, maxSize, minSize, maxFiles }} > <Button className={cn( 'relative h-auto w-full flex-col overflow-hidden p-8', isDragActive && 'outline-none ring-1 ring-ring', className )} disabled={disabled} type="button" variant="outline" {...getRootProps()} > <input {...getInputProps()} disabled={disabled} /> {children} </Button> </DropzoneContext.Provider> );};const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); } return context;};export type DropzoneContentProps = { children?: ReactNode; className?: string;};const maxLabelItems = 3;export const DropzoneContent = ({ children, className,}: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate font-medium text-sm"> {src.length > maxLabelItems ? `${new Intl.ListFormat('en').format( src.slice(0, maxLabelItems).map((file) => file.name) )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat('en').format(src.map((file) => file.name))} </p> <p className="w-full text-wrap text-muted-foreground text-xs"> Drag and drop or click to replace </p> </div> );};export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string;};export const DropzoneEmptyState = ({ children, className,}: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ''; if (accept) { caption += 'Accepts '; caption += new Intl.ListFormat('en').format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate text-wrap font-medium text-sm"> Upload {maxFiles === 1 ? 'a file' : 'files'} </p> <p className="w-full truncate text-wrap text-muted-foreground text-xs"> Drag and drop or click to upload </p> {caption && ( <p className="text-wrap text-muted-foreground text-xs">{caption}.</p> )} </div> );};
With custom empty state
'use client';import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/ui/shadcn-io/dropzone';import { UploadIcon } from 'lucide-react';import { useState } from 'react';const Example = () => { const [files, setFiles] = useState<File[] | undefined>(); const handleDrop = (files: File[]) => { console.log(files); setFiles(files); }; return ( <Dropzone onDrop={handleDrop} onError={console.error} src={files}> <DropzoneEmptyState> <div className="flex w-full items-center gap-4 p-8"> <div className="flex size-16 items-center justify-center rounded-lg bg-muted text-muted-foreground"> <UploadIcon size={24} /> </div> <div className="text-left"> <p className="font-medium text-sm">Upload a file</p> <p className="text-muted-foreground text-xs"> Drag and drop or click to upload </p> </div> </div> </DropzoneEmptyState> <DropzoneContent /> </Dropzone> );};export default Example;
'use client';import { UploadIcon } from 'lucide-react';import type { ReactNode } from 'react';import { createContext, useContext } from 'react';import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone';import { useDropzone } from 'react-dropzone';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions['accept']; maxSize?: DropzoneOptions['maxSize']; minSize?: DropzoneOptions['minSize']; maxFiles?: DropzoneOptions['maxFiles'];};const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`;};const DropzoneContext = createContext<DropzoneContextType | undefined>( undefined);export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void; children?: ReactNode;};export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props}: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( <DropzoneContext.Provider key={JSON.stringify(src)} value={{ src, accept, maxSize, minSize, maxFiles }} > <Button className={cn( 'relative h-auto w-full flex-col overflow-hidden p-8', isDragActive && 'outline-none ring-1 ring-ring', className )} disabled={disabled} type="button" variant="outline" {...getRootProps()} > <input {...getInputProps()} disabled={disabled} /> {children} </Button> </DropzoneContext.Provider> );};const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); } return context;};export type DropzoneContentProps = { children?: ReactNode; className?: string;};const maxLabelItems = 3;export const DropzoneContent = ({ children, className,}: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate font-medium text-sm"> {src.length > maxLabelItems ? `${new Intl.ListFormat('en').format( src.slice(0, maxLabelItems).map((file) => file.name) )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat('en').format(src.map((file) => file.name))} </p> <p className="w-full text-wrap text-muted-foreground text-xs"> Drag and drop or click to replace </p> </div> );};export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string;};export const DropzoneEmptyState = ({ children, className,}: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ''; if (accept) { caption += 'Accepts '; caption += new Intl.ListFormat('en').format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate text-wrap font-medium text-sm"> Upload {maxFiles === 1 ? 'a file' : 'files'} </p> <p className="w-full truncate text-wrap text-muted-foreground text-xs"> Drag and drop or click to upload </p> {caption && ( <p className="text-wrap text-muted-foreground text-xs">{caption}.</p> )} </div> );};
Showing an image preview
'use client';import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/ui/shadcn-io/dropzone';import { useState } from 'react';const Example = () => { const [files, setFiles] = useState<File[] | undefined>(); const [filePreview, setFilePreview] = useState<string | undefined>(); const handleDrop = (files: File[]) => { console.log(files); setFiles(files); if (files.length > 0) { const reader = new FileReader(); reader.onload = (e) => { if (typeof e.target?.result === 'string') { setFilePreview(e.target?.result); } }; reader.readAsDataURL(files[0]); } }; return ( <Dropzone accept={{ 'image/*': ['.png', '.jpg', '.jpeg'] }} onDrop={handleDrop} onError={console.error} src={files} > <DropzoneEmptyState /> <DropzoneContent> {filePreview && ( <div className="h-[102px] w-full"> <img alt="Preview" className="absolute top-0 left-0 h-full w-full object-cover" src={filePreview} /> </div> )} </DropzoneContent> </Dropzone> );};export default Example;
'use client';import { UploadIcon } from 'lucide-react';import type { ReactNode } from 'react';import { createContext, useContext } from 'react';import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone';import { useDropzone } from 'react-dropzone';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions['accept']; maxSize?: DropzoneOptions['maxSize']; minSize?: DropzoneOptions['minSize']; maxFiles?: DropzoneOptions['maxFiles'];};const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`;};const DropzoneContext = createContext<DropzoneContextType | undefined>( undefined);export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void; children?: ReactNode;};export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props}: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( <DropzoneContext.Provider key={JSON.stringify(src)} value={{ src, accept, maxSize, minSize, maxFiles }} > <Button className={cn( 'relative h-auto w-full flex-col overflow-hidden p-8', isDragActive && 'outline-none ring-1 ring-ring', className )} disabled={disabled} type="button" variant="outline" {...getRootProps()} > <input {...getInputProps()} disabled={disabled} /> {children} </Button> </DropzoneContext.Provider> );};const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); } return context;};export type DropzoneContentProps = { children?: ReactNode; className?: string;};const maxLabelItems = 3;export const DropzoneContent = ({ children, className,}: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate font-medium text-sm"> {src.length > maxLabelItems ? `${new Intl.ListFormat('en').format( src.slice(0, maxLabelItems).map((file) => file.name) )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat('en').format(src.map((file) => file.name))} </p> <p className="w-full text-wrap text-muted-foreground text-xs"> Drag and drop or click to replace </p> </div> );};export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string;};export const DropzoneEmptyState = ({ children, className,}: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ''; if (accept) { caption += 'Accepts '; caption += new Intl.ListFormat('en').format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return ( <div className={cn('flex flex-col items-center justify-center', className)}> <div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground"> <UploadIcon size={16} /> </div> <p className="my-2 w-full truncate text-wrap font-medium text-sm"> Upload {maxFiles === 1 ? 'a file' : 'files'} </p> <p className="w-full truncate text-wrap text-muted-foreground text-xs"> Drag and drop or click to upload </p> {caption && ( <p className="text-wrap text-muted-foreground text-xs">{caption}.</p> )} </div> );};
Use Cases
- File uploads - Document, image, and media file handling
- Profile pictures - Avatar uploads with image preview and validation
- Content management - Bulk file uploads for admin interfaces
- Form attachments - Resume uploads, document submissions
Implementation
Built on react-dropzone with file validation. Supports async upload handling. Use Context API for complex file management across components.
Combobox
Autocomplete input with search and selection for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring keyboard navigation and custom item creation.
Editor
Rich text editor component for React and Next.js applications. Built with Tiptap, TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring slash commands, syntax highlighting, and collaborative editing.