# DropZone **Category**: react **URL**: https://ui.darkcode.dev/en/docs/react/components/drop-zone **Source**: https://raw.githubusercontent.com/DarkCode-Developers/darkcode-ui/refs/heads/main/apps/docs/content/docs/en/react/components/(forms)/drop-zone.mdx > A drag-and-drop file upload area with file type filtering, size limits, and a file list preview. *** ## Import ```tsx import { DropZone } from '@darkcode-ui/react'; ``` DropZone is a composable component. You own the file state and render the file list yourself, which keeps the upload flow fully under your control. ### Usage ```tsx import {DropZone} from "@darkcode-ui/react"; export function Default() { return ( Drag files here or click to browse PDF, DOCX, PNG, or JPG up to 10 MB. Select files ); } ``` ### Anatomy Import the DropZone component and access all parts using dot notation. ```tsx import { DropZone } from '@darkcode-ui/react'; Drag files here or click to browse PDF, DOCX, PNG, or JPG up to 10 MB. Select files Proposal.pdf 2.4 MB ; ``` ### With File List Render `DropZone.FileList` with one `DropZone.FileItem` per file. The list animates items in and out as your state changes. ```tsx import {DropZone} from "@darkcode-ui/react"; export function WithFileList() { return ( Drag files here or click to browse PDF, DOCX, PNG, or JPG up to 10 MB. Select files Proposal.pdf 2.4 MB Cover.png Complete · 1.1 MB ); } ``` ### Compact File List Pass `compact` to `DropZone.FileList` for a denser layout, useful when many files are listed. ```tsx import {DropZone} from "@darkcode-ui/react"; export function CompactFileList() { return ( Drag files here or click to browse Select files Report.docx 820 KB Assets.zip 14.2 MB Hero.png 1.1 MB ); } ``` ### Disabled Set `isDisabled` on `DropZone.Area` to prevent dropping. The Trigger inside the area is disabled automatically. ```tsx import {DropZone} from "@darkcode-ui/react"; export function Disabled() { return ( Drag files here or click to browse Uploads are currently disabled. Select files ); } ``` ### Image Only Use the `accept` prop on `DropZone.Input` to restrict the file picker to specific file types. ```tsx import {DropZone} from "@darkcode-ui/react"; export function ImageOnly() { return ( Drag images here or click to browse PNG, JPG, or GIF up to 5 MB. Select images ); } ``` ### Max Size Limit Validate file size in your `onSelect` handler and mark oversized files with `status="failed"`, then offer a retry. ```tsx "use client"; import {DropZone} from "@darkcode-ui/react"; import React from "react"; const MAX_SIZE = 5 * 1024 * 1024; // 5 MB interface UploadFile { id: string; name: string; size: number; status: "complete" | "failed"; } function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const units = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`; } export function MaxSizeLimit() { const [files, setFiles] = React.useState([ {id: "seed", name: "Archive.zip", size: 14_680_064, status: "failed"}, ]); const addFiles = (list: FileList) => { const next = Array.from(list).map((file) => ({ id: `${file.name}-${Date.now()}-${Math.random()}`, name: file.name, size: file.size, status: file.size > MAX_SIZE ? ("failed" as const) : ("complete" as const), })); setFiles((prev) => [...prev, ...next]); }; const removeFile = (id: string) => setFiles((prev) => prev.filter((file) => file.id !== id)); return ( Drag files here or click to browse Each file must be 5 MB or smaller. Select files {files.map((file) => ( {file.name} {file.status === "failed" ? `Exceeds the 5 MB limit (${formatBytes(file.size)})` : formatBytes(file.size)} {file.status === "failed" ? : null} removeFile(file.id)} /> ))} ); } ``` ### Multiple Files Pass `multiple` to `DropZone.Input` and collect files from both the picker (`onSelect`) and drag-and-drop (`onDrop`). ```tsx "use client"; import {DropZone} from "@darkcode-ui/react"; import React from "react"; interface UploadFile { id: string; name: string; size: number; } function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const units = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`; } function getFormat(name: string): {color: string; format: string} { const ext = name.split(".").pop()?.toLowerCase() ?? ""; if (["png", "jpg", "jpeg", "gif", "webp"].includes(ext)) return {color: "green", format: "IMG"}; if (ext === "pdf") return {color: "red", format: "PDF"}; if (["zip", "rar", "7z"].includes(ext)) return {color: "amber", format: "ZIP"}; return {color: "blue", format: ext ? ext.toUpperCase().slice(0, 3) : "DOC"}; } export function MultipleFiles() { const [files, setFiles] = React.useState([]); const addFiles = (incoming: File[]) => { const next = incoming.map((file) => ({ id: `${file.name}-${Date.now()}-${Math.random()}`, name: file.name, size: file.size, })); setFiles((prev) => [...prev, ...next]); }; return ( { const dropped = await Promise.all( event.items .filter((item) => item.kind === "file") .map((item) => (item as unknown as {getFile: () => Promise}).getFile()), ); addFiles(dropped); }} > Drag files here or click to browse Upload as many files as you need. Select files addFiles(Array.from(list))} /> {files.map((file) => { const meta = getFormat(file.name); return ( {file.name} {formatBytes(file.size)} setFiles((prev) => prev.filter((item) => item.id !== file.id))} /> ); })} ); } ``` ### Custom Icon Provide children to `DropZone.Icon` to replace the default cloud upload icon. ```tsx import {DropZone} from "@darkcode-ui/react"; export function CustomIcon() { return ( Drop your documents here We accept PDF and Word files. Browse files ); } ``` ### Custom Triggers The Trigger, retry, and remove buttons all accept `className` and custom children for full control over their appearance. ```tsx import {DropZone} from "@darkcode-ui/react"; export function CustomTriggers() { return ( Drag files here or click to browse Upload from device Invoice.pdf Upload failed Retry Remove ); } ``` ### useDropZone Hook The `useDropZone` hook manages the whole upload lifecycle — file state, validation, image previews, and uploads — so you only render the UI. Spread `getAreaProps()` onto `DropZone.Area` and `getInputProps()` onto `DropZone.Input`, then map `files` to `DropZone.FileItem`. Rejections (wrong type, too large, too many) are reported via `onFilesRejected`. ```tsx "use client"; import {DropZone, formatFileSize, useDropZone} from "@darkcode-ui/react"; import React from "react"; function badge(name: string): {color: string; format: string} { const ext = name.split(".").pop()?.toUpperCase().slice(0, 4) ?? "FILE"; if (/\.(png|jpe?g|gif|webp)$/i.test(name)) return {color: "green", format: "IMG"}; if (/\.pdf$/i.test(name)) return {color: "red", format: "PDF"}; return {color: "gray", format: ext}; } export function WithValidation() { const [rejection, setRejection] = React.useState(null); const dz = useDropZone({ accept: "image/*,.pdf", maxFiles: 3, maxSize: 2 * 1024 * 1024, onFilesAccepted: () => setRejection(null), onFilesRejected: (rejections) => setRejection(rejections[0]?.reason ?? null), }); return ( Drag files here or click to browse Images or PDF, up to 2 MB each (max 3 files). Select files {rejection ?

{rejection}

: null} {dz.files.map((file) => { const meta = badge(file.name); return ( {file.name} {formatFileSize(file.size)} dz.removeFile(file.id)} /> ); })}
); } ``` ### Image Previews `DropZone.FilePreview` renders an image thumbnail from a `File` or a URL string, with automatic object-URL cleanup and a graceful fallback (a file format icon) for non-images or load errors. `useDropZone` populates `file.preview` for images automatically. ```tsx "use client"; import {DropZone, formatFileSize, useDropZone} from "@darkcode-ui/react"; export function ImagePreview() { const dz = useDropZone({accept: "image/*", maxFiles: 6}); return ( Drag images here or click to browse Thumbnails are generated for each image. Select images {dz.files.map((file) => ( {file.name} {formatFileSize(file.size)} dz.removeFile(file.id)} /> ))} ); } ``` ### Upload Progress Pass `onUpload` (or `uploadUrl`) to `useDropZone` to upload each accepted file with live progress. Failed uploads surface a retry, and removing a file aborts its upload. For real uploads, use the exported `uploadFile` helper — it is `XMLHttpRequest`-based, so it reports true progress and supports `AbortSignal`. ```tsx "use client"; import type {DropZoneFile, UseDropZoneUploadHelpers} from "@darkcode-ui/react"; import {DropZone, formatFileSize, useDropZone} from "@darkcode-ui/react"; function badge(name: string): {color: string; format: string} { if (/\.(png|jpe?g|gif|webp)$/i.test(name)) return {color: "green", format: "IMG"}; if (/\.pdf$/i.test(name)) return {color: "red", format: "PDF"}; if (/\.(zip|rar|7z)$/i.test(name)) return {color: "amber", format: "ZIP"}; return {color: "blue", format: name.split(".").pop()?.toUpperCase().slice(0, 4) ?? "FILE"}; } // Simulates a network upload with progress events (no backend needed). For a real // upload, pass `uploadUrl` to useDropZone or call the exported `uploadFile` helper. function simulateUpload(_file: DropZoneFile, {onProgress, signal}: UseDropZoneUploadHelpers) { return new Promise((resolve, reject) => { let progress = 0; const interval = setInterval(() => { if (signal.aborted) { clearInterval(interval); reject(new DOMException("Aborted", "AbortError")); return; } progress += 8 + Math.random() * 16; if (progress >= 100) { clearInterval(interval); // ~20% chance to fail so the retry affordance is visible. if (Math.random() < 0.2) { reject(new Error("Network error — tap retry")); } else { onProgress(100); resolve(); } } else { onProgress(progress); } }, 320); signal.addEventListener("abort", () => clearInterval(interval)); }); } export function UploadProgress() { const dz = useDropZone({multiple: true, onUpload: simulateUpload}); return ( Drag files here or click to browse Uploads start automatically with live progress. Select files {dz.files.map((file) => { const meta = badge(file.name); return ( {file.name} {file.status === "uploading" ? `Uploading… ${Math.round(file.progress)}%` : file.status === "failed" ? file.error : formatFileSize(file.size)} {file.status === "uploading" ? ( ) : null} {file.status === "failed" ? ( dz.retryFile(file.id)} /> ) : null} dz.removeFile(file.id)} /> ); })} ); } ``` ### Directory & Paste Set `acceptDirectory` on `DropZone.Input` to select an entire folder (`webkitdirectory`), or `capture` for the mobile camera. Clipboard paste (⌘/Ctrl + V) is supported automatically when the area is focused — pasted files flow through the same `onDrop` handler. ```tsx "use client"; import {DropZone, formatFileSize, useDropZone} from "@darkcode-ui/react"; function badge(name: string): {color: string; format: string} { if (/\.(png|jpe?g|gif|webp)$/i.test(name)) return {color: "green", format: "IMG"}; if (/\.pdf$/i.test(name)) return {color: "red", format: "PDF"}; return {color: "gray", format: name.split(".").pop()?.toUpperCase().slice(0, 4) ?? "FILE"}; } export function DirectoryUpload() { const dz = useDropZone({multiple: true}); return ( Choose a folder, drop files, or paste The whole directory is added. Paste images with ⌘/Ctrl + V. Choose folder {dz.files.map((file) => { const meta = badge(file.name); return ( {file.name} {formatFileSize(file.size)} dz.removeFile(file.id)} /> ); })} {dz.files.length > 0 ? (

{dz.files.length} files · {formatFileSize(dz.totalSize)}

) : null}
); } ``` ## Related Components - **ProgressBar**: Shows determinate or indeterminate progress of an operation - **Button**: Allows a user to perform an action - **Label**: Accessible label for form controls ## Styling ### Customizing the component classes To customize the DropZone component classes, you can use the `@layer components` directive.
[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes). ```css @layer components { .drop-zone__area { @apply rounded-xl border-foreground/20; } .drop-zone__file-item { @apply bg-default/40; } } ``` DarkCode UI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize. ### CSS Classes The DropZone component uses these CSS classes ([View source styles](https://github.com/DarkCode-Developers/darkcode-ui/blob/main/packages/styles/components/drop-zone.css)): #### Base Classes - `.drop-zone` - Root wrapper #### Element Classes - `.drop-zone__area` - The droppable area (RAC DropZone) - `.drop-zone__icon` - Upload icon container - `.drop-zone__label` - Primary label text - `.drop-zone__description` - Secondary description text - `.drop-zone__trigger` - Browse button - `.drop-zone__input` - Hidden file input - `.drop-zone__file-list` - File list container with animation - `.drop-zone__file-item` - Individual file entry - `.drop-zone__file-format-icon` - File type SVG icon with colored badge - `.drop-zone__file-info` - File name and metadata container - `.drop-zone__file-name` - File name text - `.drop-zone__file-meta` - File size/status metadata - `.drop-zone__file-progress` - Upload progress bar - `.drop-zone__file-retry-trigger` - Retry button for failed uploads - `.drop-zone__file-remove-trigger` - Remove/close button #### Modifier Classes - `.drop-zone__file-list--compact` - Denser file list layout - `.drop-zone__file-format-icon--{color}` - Badge color (`gray`, `red`, `orange`, `amber`, `green`, `blue`, `purple`) #### Interactive States - **Drop target**: `[data-drop-target="true"]` on `.drop-zone__area` (accent border and background) - **Focus visible**: `[data-focus-visible="true"]` on `.drop-zone__area` (focus ring) - **Disabled**: `[data-disabled="true"]` on `.drop-zone__area` (reduced opacity) - **File status**: `[data-status="uploading|complete|failed"]` on `.drop-zone__file-item` ## API Reference ### DropZone The root wrapper component. Provides file picker context. | Prop | Type | Default | Description | |------|------|---------|-------------| | `children` | `ReactNode` | - | Area, FileList, and other sub-components | | `className` | `string` | - | Additional CSS class | | `render` | `DOMRenderFunction` | - | Custom render function to override the default `div` element | Also supports all native `div` HTML attributes. ### DropZone.Area The droppable area. Wraps RAC [DropZone](https://react-spectrum.adobe.com/react-aria/DropZone.html). Also supports all RAC [DropZone](https://react-spectrum.adobe.com/react-aria/DropZone.html) props (`onDrop`, `getDropOperation`, `isDisabled`, etc.). ### DropZone.Icon Upload icon container. Renders a default cloud upload icon when no children are provided. ### DropZone.Label Primary label text. Wraps RAC [Text](https://react-spectrum.adobe.com/react-aria/Text.html) with `slot="label"`. ### DropZone.Description Secondary description text. ### DropZone.Input Hidden file input for the browse trigger. | Prop | Type | Default | Description | |------|------|---------|-------------| | `accept` | `string` | - | Accepted file types (e.g. `"image/*,.pdf"`) | | `multiple` | `boolean` | - | Whether multiple files can be selected | | `acceptDirectory` | `boolean` | - | Allow selecting an entire directory (sets `webkitdirectory`) | | `capture` | `boolean \| "user" \| "environment"` | - | Mobile capture mode for the camera/microphone | | `onSelect` | `(files: FileList) => void` | - | Called when files are selected via the file picker | ### DropZone.Trigger Browse button that opens the file picker. Wraps RAC [Button](https://react-spectrum.adobe.com/react-aria/Button.html). When no children are provided, it renders `Select files`. ### DropZone.FileList Animated file list container using Motion `AnimatePresence`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `compact` | `boolean` | `false` | Renders a denser layout for the file rows | ### DropZone.FileItem Individual file entry with Motion layout animations. | Prop | Type | Default | Description | |------|------|---------|-------------| | `status` | `'uploading' \| 'complete' \| 'failed'` | - | Upload status of this file item | ### DropZone.FileFormatIcon File type SVG icon with a colored format badge. | Prop | Type | Default | Description | |------|------|---------|-------------| | `format` | `string` | - | File format label (e.g. `"PDF"`, `"JPG"`) | | `color` | `string` | `'gray'` | Badge color (`gray`, `red`, `orange`, `amber`, `green`, `blue`, `purple`) | ### DropZone.FilePreview Image thumbnail for a file. Renders an `` from a `File` (creating and revoking an object URL automatically) or a URL string, and falls back to a file format icon for non-images or load errors. | Prop | Type | Default | Description | |------|------|---------|-------------| | `src` | `File \| string` | - | A `File` (object URL is managed for you) or an image URL | | `fallback` | `ReactNode` | `` | Rendered when there is no preview or the image fails to load | Also supports all native `img` HTML attributes. ### DropZone.FileInfo Container for file name and metadata. ### DropZone.FileName File name text display. ### DropZone.FileMeta File size/status metadata text. ### DropZone.FileProgress Upload progress bar. Wraps DarkCode UI [ProgressBar](/docs/react/components/progress-bar). | Prop | Type | Default | Description | |------|------|---------|-------------| | `size` | `string` | `'sm'` | Progress bar size | Also supports all ProgressBar props. ### DropZone.FileProgressTrack Progress bar track. Wraps `ProgressBar.Track`. ### DropZone.FileProgressFill Progress bar fill. Wraps `ProgressBar.Fill`. ### DropZone.FileRetryTrigger Retry button for failed uploads. Wraps RAC [Button](https://react-spectrum.adobe.com/react-aria/Button.html). ### DropZone.FileRemoveTrigger Remove/close button. Wraps RAC [Button](https://react-spectrum.adobe.com/react-aria/Button.html). ## Hooks & helpers ### useDropZone A headless hook that manages file state, validation, image previews, and uploads. Returns the file list and prop getters to wire onto the presentational parts. ```tsx import { useDropZone } from '@darkcode-ui/react'; const dz = useDropZone({ accept: 'image/*,.pdf', maxSize: 5 * 1024 * 1024, maxFiles: 5, onUpload: (file, { onProgress, signal }) => upload(file, onProgress, signal), }); // // // dz.files.map(...) → dz.removeFile(id), dz.retryFile(id) ``` #### Options | Option | Type | Default | Description | |------|------|---------|-------------| | `files` | `DropZoneFile[]` | - | Controlled file state | | `defaultFiles` | `DropZoneFile[]` | `[]` | Initial (uncontrolled) file state | | `onFilesChange` | `(files: DropZoneFile[]) => void` | - | Called whenever the file list changes | | `accept` | `string` | - | Accepted file types (same syntax as the input `accept`) | | `maxSize` | `number` | - | Maximum size per file, in bytes | | `minSize` | `number` | - | Minimum size per file, in bytes | | `maxFiles` | `number` | - | Maximum number of files allowed in total | | `multiple` | `boolean` | `maxFiles !== 1` | Whether multiple files can be selected | | `validate` | `(file: File) => string \| null` | - | Custom validator; return a message to reject | | `disabled` | `boolean` | `false` | Disable selection, drop, and validation | | `generatePreviews` | `boolean` | `true` | Create object-URL previews for image files | | `onFilesAccepted` | `(files: DropZoneFile[]) => void` | - | Called with the files that were added | | `onFilesRejected` | `(rejections: DropZoneRejection[]) => void` | - | Called with the files that failed validation | | `onUpload` | `(file, helpers) => Promise` | - | Upload implementation per file (`helpers`: `onProgress`, `signal`) | | `uploadUrl` | `string` | - | Convenience endpoint; uploads via `uploadFile` when `onUpload` is not set | | `uploadOptions` | `UploadFileOptions` | - | Extra options forwarded to `uploadFile` when `uploadUrl` is used | #### Returns | Property | Type | Description | |------|------|-------------| | `files` | `DropZoneFile[]` | Current files | | `isDisabled` | `boolean` | Whether the drop zone is disabled | | `totalSize` | `number` | Combined byte size of all files | | `addFiles` | `(files: FileList \| File[]) => void` | Validate and add files | | `removeFile` | `(id: string) => void` | Remove a file (aborts upload, revokes preview) | | `retryFile` | `(id: string) => void` | Retry a failed/aborted upload | | `updateFile` | `(id: string, patch: Partial) => void` | Patch a single file entry | | `clear` | `() => void` | Remove all files | | `getInputProps` | `() => object` | Props to spread onto `DropZone.Input` | | `getAreaProps` | `() => object` | Props to spread onto `DropZone.Area` (also enables paste) | `DropZoneFile` is `{ id, file, name, size, type, status, progress, error?, preview? }`, where `status` is `'pending' | 'uploading' | 'complete' | 'failed'`. ### uploadFile `XMLHttpRequest`-based upload helper with real progress events and abort support. Pass it to `useDropZone`'s `onUpload`, or call it directly. ```tsx import { uploadFile } from '@darkcode-ui/react'; await uploadFile(file, { url: '/api/upload', onProgress: ({ percent }) => setProgress(percent ?? 0), signal: controller.signal, }); ``` | Option | Type | Default | Description | |------|------|---------|-------------| | `url` | `string` | - | Endpoint the file is uploaded to | | `method` | `string` | `"POST"` | HTTP method | | `fieldName` | `string` | `"file"` | Form field name for the file part | | `headers` | `Record` | - | Additional request headers | | `data` | `Record` | - | Extra fields appended to the `FormData` | | `withCredentials` | `boolean` | `false` | Send cookies/credentials cross-origin | | `signal` | `AbortSignal` | - | Cancels the in-flight upload | | `onProgress` | `(progress) => void` | - | Progress callback (`{ loaded, total, percent }`) | ### formatFileSize `formatFileSize(bytes, decimals?)` formats a byte count into a human-readable string (e.g. `2.4 MB`).