Inputs

Dropzone

A drag-and-drop file picker built on a native file input — per-file accept, size and count validation with visible rejection reasons.

Preview in your theme

Loading preview…

Dependencies
lucide-react
Registry dependencies
utils
"use client"

import * as React from "react"
import { FileIcon, UploadCloud, X } from "lucide-react"
import { cn } from "@/lib/utils"

interface Rejection {
  name: string
  reason: string
}

export interface DropzoneProps
  extends Omit<
    React.HTMLAttributes<HTMLDivElement>,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/dropzone.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "Dropzone" component (lucide-react
icons; no react-dropzone — native drag & drop plus a native
<input type="file">).

Contract
- Export a forwardRef<HTMLDivElement> component extending div props with
  onDrop/onDragEnter/onDragLeave/onDragOver omitted; spread the rest on the
  root and merge className via cn().
- Props: onFiles(files: File[]) => void — fired with the FULL current
  accepted list after every add or remove (selection only, never uploads);
  accept?: string (native accept syntax: "image/*", ".pdf", "video/mp4",
  comma-separated); multiple?: boolean (default true; false = single-file
  mode where a new pick replaces the old one); maxFiles?: number;
  maxSize?: number (bytes); disabled?: boolean.
- Internal state: accepted File[] plus the latest batch's rejections as
  {name, reason}[].

Behavior
- The prompt area is a real <button type="button"> that .click()s a hidden
  native file input — Enter/Space and screen-reader semantics come free; the
  input keeps accept/multiple so the OS picker pre-filters.
- Reset input.value after reading the FileList so re-picking the same file
  re-fires change.
- Drag: dragenter/dragleave fire once per child node crossed, so track a
  depth counter in a ref — highlight while depth > 0, clear on drop or when
  the counter returns to zero. preventDefault on dragover to permit
  dropping — also while disabled, where dataTransfer.dropEffect = "none"
  shows the no-drop cursor: an uncancelled dragover hands the drop to the
  browser, which opens the file and navigates away from unsaved form state.
  Only react to drags whose dataTransfer.types include "Files".
- Validate per file, in order: accept match (extension ".png", wildcard
  "image/*", or exact MIME) → maxSize → remaining capacity (maxFiles minus
  already-accepted; in single-file mode the new file replaces instead of
  consuming capacity). Passing files append to the list and onFiles fires
  with the updated list; failing files render as reason lines inside the
  zone ("logo.svg — file type not accepted", "video.mp4 — larger than
  2 MB", "d.png — file limit reached (max 3)"). Each new selection replaces
  the previous batch's rejection lines.
- Every accepted file renders as a row: name (truncated) + human-readable
  size (B/KB/MB/GB) + a remove button; removing updates the list and
  re-fires onFiles. The pressed button unmounts with its row, so an effect
  after the list changes moves focus to the remove button now at that index,
  else the new last row's, else the browse button — never to <body>.
- disabled: trigger and remove buttons get the disabled attribute,
  dragenter/dragleave early-return, dragover and drop still preventDefault but
  add nothing, the zone dims.

Rendering & styling
- Semantic tokens only. Idle zone: border border-dashed rounded-xl
  bg-muted/30; drag-over: border-primary bg-primary/5 (also exposed as a
  data-dragover attribute for consumer styling); file rows: bg-background +
  border; rejection lines: text-destructive; icon and captions:
  text-muted-foreground. No hex/oklch anywhere.
- UploadCloud icon above a headline ("Drag & drop files here, or click to
  browse" → "Drop files to add them" while dragging) and a hint caption
  derived from accept/maxSize/maxFiles ("image/* · up to 2 MB · max 3
  files").
- focus-visible ring on the trigger and remove buttons; remove buttons get
  aria-label="Remove <name>"; rejection lines live inside an always-mounted
  aria-live="polite" container so screen readers announce them; the color
  transition is motion-reduce:transition-none.

Customization levers
- Controlled list: swap the internal useState for value/onFilesChange props
  when a form library owns the files — the validation pipeline and rendering
  stay identical.
- Real upload: keep transport in the consumer — const fd = new FormData();
  files.forEach(f => fd.append("files", f));
  fetch("/api/upload", { method: "POST", body: fd }). The component never
  uploads.
- Single-file mode: multiple={false} for avatar/document pickers — new picks
  replace, no capacity juggling.
- Rejection tone: text-destructive is the only error token; remap it (e.g.
  to a warning token) without touching validation logic.
- Density: the p-4 zone padding, py-8 prompt padding and rounded-xl radius
  are the layout knobs; compact forms read fine at p-3/py-6/rounded-lg.
- Hint caption: derived from the constraint props — override the copy or
  drop the line freely; validation does not depend on it.

Concepts

  • Selection, not transport — the component owns picking, validating and listing; onFiles hands the current accepted list to the consumer, whose own submit logic performs the actual upload.
  • Native input delegation — a real <input type="file"> stays in the DOM and a real <button> triggers it, so keyboard activation, focus rings and the OS picker's accept pre-filter all come for free.
  • Drag-depth counter — dragenter/dragleave fire once per child node crossed; counting depth in a ref and clearing the highlight only at zero kills the flicker a boolean toggle produces.
  • Per-file gating — each incoming file passes accept → size → capacity checks in order; the first failure becomes a visible reason line instead of a silent drop.
  • Append with capacity — reselecting adds to the existing list until maxFiles is reached; removing a file frees capacity, and single-file mode replaces instead of appending.

On This Page