Dropzone
Dropzone lets users upload files into a UI
Usage
Use a dropzone when the user needs to upload an image or file.
Instalation
Run the following command to install the react-dropzone
package:
npm i react-dropzone
Copy and paste the following code into your project.
'use client'
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'
import {
useDropzone,
type Accept,
type DropzoneProps as PrimitiveDropzoneProps,
} from 'react-dropzone'
import { createSafeContext } from '@/lib/createSafeContext'
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'
export const MIME_TYPES = {
// Images
png: 'image/png',
gif: 'image/gif',
jpeg: 'image/jpeg',
svg: 'image/svg+xml',
webp: 'image/webp',
avif: 'image/avif',
// Documents
mp4: 'video/mp4',
zip: 'application/zip',
csv: 'text/csv',
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
exe: 'application/vnd.microsoft.portable-executable',
}
export const IMAGE_MIME_TYPE = [
MIME_TYPES.png,
MIME_TYPES.gif,
MIME_TYPES.jpeg,
MIME_TYPES.svg,
MIME_TYPES.webp,
MIME_TYPES.avif,
]
export const PDF_MIME_TYPE = [MIME_TYPES.pdf]
export const MS_WORD_MIME_TYPE = [MIME_TYPES.doc, MIME_TYPES.docx]
export const MS_EXCEL_MIME_TYPE = [MIME_TYPES.xls, MIME_TYPES.xlsx]
export const MS_POWERPOINT_MIME_TYPE = [MIME_TYPES.ppt, MIME_TYPES.pptx]
export const EXE_MIME_TYPE = [MIME_TYPES.exe]
type DropzoneContextValue = {
loading: boolean
idle: boolean
accepted: boolean
rejected: boolean
}
const [DropZoneProvider, useDropzoneState] = createSafeContext<
DropzoneContextValue & { id: string }
>({ name: 'DropzoneContext' })
export interface DropzoneProps
extends Omit<PrimitiveDropzoneProps, 'children' | 'accept'>,
VariantProps<typeof dropzoneVariants> {
className?: string
loading?: boolean
name?: string
children?: React.ReactNode | ((state: DropzoneContextValue) => JSX.Element)
accept?: Accept | string[]
/** Get open function as ref. Useful for open the files dialog from an external button */
openRef?: React.ForwardedRef<() => void | undefined>
}
const dropzoneVariants = cva(
'group relative cursor-pointer w-full text-center overflow-hidden rounded-sm p-4 outline-none transition-all active:scale-[0.98] peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
{
variants: {
variant: {
filled:
'bg-surfaceContainer data-[accepted=true]:bg-success/12 data-[rejected=true]:bg-error/12',
outlined:
'outline outline-offset-0 outline-outlineVariant data-[accepted=true]:outline-2 data-[rejected=true]:outline-2 data-[accepted=true]:outline-primary data-[rejected=true]:outline-error',
},
},
defaultVariants: {
variant: 'filled',
},
}
)
export const defaultProps: Partial<DropzoneProps> = {
loading: false,
multiple: false,
maxSize: Infinity,
autoFocus: false,
noClick: false, // disable or enable click to open files
noDrag: false,
noDragEventsBubbling: false,
noKeyboard: false,
useFsAccessApi: true,
}
const DropzoneLoading = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const { loading } = useDropzoneState()
if (!loading) return null
return <div ref={ref} {...props} />
})
DropzoneLoading.displayName = 'DropzoneLoading'
const DropzoneIdle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { idle } = useDropzoneState()
if (!idle) return null
return (
<div
ref={ref}
className={cn('animate-in zoom-in-50', className)}
{...props}
/>
)
})
DropzoneIdle.displayName = 'DropzoneIdle'
const DropzoneAccept = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { accepted } = useDropzoneState()
if (!accepted) return null
return (
<div
ref={ref}
className={cn('text-onSuccessContainer animate-in zoom-in-50', className)}
{...props}
/>
)
})
DropzoneAccept.displayName = 'DropzoneAccept'
const DropzoneReject = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { rejected } = useDropzoneState()
if (!rejected) return null
return (
<div
ref={ref}
className={cn('text-onErrorContainer animate-in zoom-in-50', className)}
{...props}
/>
)
})
DropzoneReject.displayName = 'DropzoneReject'
const DropzoneContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { accepted, rejected } = useDropzoneState()
return (
<div
ref={ref}
className={cn(
'relative z-10 text-onSurface',
accepted && 'text-onSuccessContainer',
rejected && 'text-onErrorContainer',
className
)}
{...props}
/>
)
})
DropzoneContent.displayName = 'DropzoneContent'
const DropzonePreview = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
'relative mr-4 grid aspect-square h-16 shrink-0 place-items-center overflow-hidden rounded-xs border border-outline/12 bg-surface text-onSurfaceVariant',
className
)}
{...props}
/>
)
})
DropzonePreview.displayName = 'DropzonePreview'
const DropzoneLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { accepted, rejected, id } = useDropzoneState()
return (
<Label
ref={ref}
className={cn(
'mb-3 text-label-lg text-onSurfaceVariant',
accepted && 'text-primary',
rejected && 'text-error',
className
)}
htmlFor={id}
{...props}
/>
)
})
DropzoneLabel.displayName = 'DropzoneLabel'
const DropzoneRoot = React.forwardRef<HTMLInputElement, DropzoneProps>(
(
{
accept,
children,
className,
disabled,
loading = false,
name,
variant,
openRef,
...props
},
ref
) => {
const randomId = React.useId()
const id = name ?? randomId
const isDisabled = disabled || loading
const { getRootProps, getInputProps, isDragAccept, isDragReject, open } =
useDropzone({
disabled: isDisabled,
accept: Array.isArray(accept)
? accept.reduce((r, key) => ({ ...r, [key]: [] }), {})
: accept,
...defaultProps,
...props,
})
if (
typeof openRef === 'object' &&
openRef !== null &&
'current' in openRef
) {
openRef.current = open
}
const isIdle = !isDragAccept && !isDragReject
const dropzoneState = {
loading,
accepted: isDragAccept,
rejected: isDragReject,
idle: isIdle,
}
return (
<DropZoneProvider value={{ id, ...dropzoneState }}>
<input
ref={ref}
{...getInputProps()}
id={id}
name={name}
className="peer"
disabled={isDisabled}
/>
<div
{...getRootProps()}
data-accepted={dropzoneState.accepted}
data-rejected={dropzoneState.rejected}
data-idle={dropzoneState.idle}
data-loading={dropzoneState.loading}
className={cn(dropzoneVariants({ variant, className }))}
>
{typeof children === 'function' ? children(dropzoneState) : children}
{isIdle && !isDisabled && (
<span
className={cn(
'absolute inset-0 z-0 bg-onSurface opacity-0 transition-opacity group-hover:opacity-4 group-active:opacity-8'
)}
/>
)}
</div>
</DropZoneProvider>
)
}
)
DropzoneRoot.displayName = 'Dropzone'
const DropZone = Object.assign(DropzoneRoot, {
Idle: DropzoneIdle,
Accept: DropzoneAccept,
Reject: DropzoneReject,
Content: DropzoneContent,
Preview: DropzonePreview,
Label: DropzoneLabel,
})
export {
DropZone,
DropzoneRoot,
DropzoneIdle,
DropzoneAccept,
DropzoneReject,
DropzoneContent,
DropzonePreview,
DropzoneLabel,
}
Update the import paths to match your project setup.
Examples
Single file
image
Click to upload an Image. Or drag and drop
Allowed formats: .png, .jpeg, .webp
import { useState } from 'react'
import { DropZone, IMAGE_MIME_TYPE } from '@/components/ui/drop-zone'
import { Icon } from '@/components/ui/icon'
export const SingleFileDropzone = () => {
const [image, setImage] = useState<File | null>(null)
return (
<DropZone
accept={IMAGE_MIME_TYPE}
onDropAccepted={(files) => {
setImage(files[0])
}}
>
<DropZone.Content>
<DropZone.Accept>
<Icon symbol="thumb_up" className="text-6xl opacity-60" />
</DropZone.Accept>
<DropZone.Reject>
<Icon symbol="thumb_down" className="text-6xl opacity-60" />
</DropZone.Reject>
<DropZone.Idle>
{image ? (
<img
src={URL.createObjectURL(image)}
alt="Preview"
className="animate-in zoom-in-0 mx-auto mb-4 aspect-square h-[60px] rounded-sm object-cover object-center"
/>
) : (
<Icon symbol="image" className="text-6xl opacity-60" />
)}
</DropZone.Idle>
<div className="space-y-2">
<p>Click to upload an Image. Or drag and drop</p>
<p className="text-body-sm opacity-60">
Allowed formats: .png, .jpeg, .webp
</p>
</div>
</DropZone.Content>
</DropZone>
)
}
Multiple files
upload_file
Click to upload files. Or drag and drop
Allowed formats: .png, .jpeg, .webp, .pdf
import { useState } from 'react'
import { parseFileSize } from '@/lib/utils'
import {
DropZone,
IMAGE_MIME_TYPE,
PDF_MIME_TYPE,
} from '@/components/ui/drop-zone'
import { Icon } from '@/components/ui/icon'
import { IconButton } from '@/components/ui/icon-button'
export const MultipleFilesDropzone = () => {
const [files, setFiles] = useState<File[]>([])
return (
<DropZone
multiple
accept={[...IMAGE_MIME_TYPE, ...PDF_MIME_TYPE]}
onDropAccepted={(files) => {
setFiles(files)
}}
>
<DropZone.Content>
<DropZone.Accept>
<Icon symbol="thumb_up" className="text-6xl opacity-60" />
</DropZone.Accept>
<DropZone.Reject>
<Icon symbol="thumb_down" className="text-6xl opacity-60" />
</DropZone.Reject>
<DropZone.Idle>
<Icon symbol="upload_file" className="text-6xl opacity-60" />
</DropZone.Idle>
<div className="space-y-2">
<p>Click to upload files. Or drag and drop</p>
<p className="text-body-sm opacity-60">
Allowed formats: .png, .jpeg, .webp, .pdf
</p>
</div>
</DropZone.Content>
</DropZone>
{files.length > 0 && (
<div className="w-full space-y-2 p-4">
{files.map((file) => (
<div key={file.name} className="flex items-center text-start">
<DropZone.Preview>
{(() => {
if (PDF_MIME_TYPE.includes(file.type)) {
return (
<Icon
symbol="description"
className="text-3xl opacity-60"
/>
)
}
if (IMAGE_MIME_TYPE.includes(file.type)) {
return (
<img
src={URL.createObjectURL(file)}
alt="Preview"
className="animate-in zoom-in-0 aspect-square h-full object-cover object-center"
/>
)
}
return (
<Icon symbol="folder" className="text-3xl opacity-60" />
)
})()}
</DropZone.Preview>
<div>
<p className="text-title-md">{file.name}</p>
<p className="text-body-sm text-onSurfaceVariant">
{parseFileSize({ size: file.size, metric: 'MB' })} MB
</p>
</div>
<IconButton
variant="standard"
className="ml-auto opacity-50 hover:opacity-100"
onClick={() => setFiles(files.filter((f) => f !== file))}
>
<Icon symbol="close" />
</IconButton>
</div>
))}
</div>
)}
)
}
Outlined dropzone
image
Click to upload an Image. Or drag and drop
Allowed formats: .png, .jpeg, .webp
import { useState } from 'react'
import { DropZone, IMAGE_MIME_TYPE } from '@/components/ui/drop-zone'
import { Icon } from '@/components/ui/icon'
export const OutlinedDropzone = () => {
const [image, setImage] = useState<File | null>(null)
return (
<DropZone
variant="outlined"
accept={IMAGE_MIME_TYPE}
onDropAccepted={(files) => {
setImage(files[0])
}}
>
<DropZone.Content>
<DropZone.Accept>
<Icon symbol="thumb_up" className="text-6xl opacity-60" />
</DropZone.Accept>
<DropZone.Reject>
<Icon symbol="thumb_down" className="text-6xl opacity-60" />
</DropZone.Reject>
<DropZone.Idle>
{image ? (
<img
src={URL.createObjectURL(image)}
alt="Preview"
className="animate-in zoom-in-0 mx-auto mb-4 aspect-square h-[60px] rounded-sm object-cover object-center"
/>
) : (
<Icon symbol="image" className="text-6xl opacity-60" />
)}
</DropZone.Idle>
<div className="space-y-2">
<p>Click to upload an Image. Or drag and drop</p>
<p className="text-body-sm opacity-60">
Allowed formats: .png, .jpeg, .webp
</p>
</div>
</DropZone.Content>
</DropZone>
)
}