Dropzone
File acquisition — drop, browse, or paste — as a thin shell over the use-file-drop hook; the drop surface is the named, focusable control.
- Status
- Since
0.4.0- Accessibility pattern
- button-role drop surface
Last updated
Drop images here
or click to browse — PNG, JPG, or WebP
Install
Add Dropzone from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/dropzoneThe same command installs the registry items it composes: @vegastack/use-file-drop.
Usage
import { Dropzone } from "@/components/ui/dropzone";
<Dropzone
accept={{ "image/*": [".png", ".jpg"] }}
onFilesAccepted={(files) => stageUploads(files)}
>
<Empty size="sm" variant="dashed">
<EmptyHeader>
<EmptyTitle>Drop images here</EmptyTitle>
<EmptyDescription>or click to browse</EmptyDescription>
</EmptyHeader>
</Empty>
</Dropzone>;Dropzone is acquisition only — everything behavioural lives in the
use-file-drop hook (over the sanctioned react-dropzone engine), and
everything after acquisition belongs to
Attachment, which "owns no upload logic, only
the visual state machine". The two meet at a plain File[] callback.
Scope
| Behaviour | Where it lives |
|---|---|
| Upload lifecycle, progress | Host + Attachment's state machine |
| Drop/paste/browse behaviour | use-file-drop — compose it directly for chrome-less surfaces (a rich-text composer accepting pasted images) |
| The dashed idle affordance | Children — compose Empty variant="dashed", "the classic drop-zone look" |
| Rendering staged files | Host, never internal — no attachments prop by design |
Examples
Typed rejections
Refusals arrive as FileDropRejection — the file plus every reason
(file-too-large, file-invalid-type, too-many-files, …), aligned with the
per-file state vocabulary Attachment renders.
Files up to 200 KB
Anything larger is refused with a typed reason
Drag-over states
The surface carries the drag feedback — an inset primary stroke while a valid payload hovers,
an inset destructive one when it cannot be accepted — so it works with any child, not only an
Empty. An absolutely positioned border-box paints the stroke one stroke-width inside the surface,
so it costs no layout and stays within WebKit's scrollable box. An Empty variant="dashed" child can
tint its dashed border in step through group-data-dragging/dropzone.
dragState paints the two states without a live DataTransfer, which neither a static example nor
an automated check can synthesise. It is presentation only: a real drag always wins.
Release to upload
A valid payload is hovering
Not accepted
This file type is refused
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | — | The idle affordance — typically Empty variant="dashed" content. |
onFilesAccepted* | (files: File[]) => void | — | Receives the accepted files of each drop/paste/browse batch. |
accept | Accept | — | Accepted types, MIME-to-extensions (react-dropzone's Accept shape):
{ "image/*": [".png", ".jpg"] }. |
aria-label | string | "Upload files" | Accessible name for the drop surface (the focusable control). |
className | string | — | Extra classes for the drop surface. |
disabled | boolean | false | Disable acquisition entirely. |
dragState | "drag-invalid" | "dragging" | — | Force the drag-over presentation without a real drag. A drag-over state can only be produced
by a live DataTransfer, which a static documentation example and the behaviour-contract lane
cannot synthesise — so the two states would otherwise be undocumented and unverified. It
paints only: the engine still owns the real data-dragging/data-drag-invalid attributes and
a live drag always wins over this prop. |
maxFiles | number | — | Maximum files per batch. |
maxSize | number | — | Per-file maximum size in bytes. |
minSize | number | — | Per-file minimum size in bytes. |
multiple | boolean | true | Allow more than one file per batch. |
onFilesRejected | ((rejections: FileDropRejection[]) => void) | — | Receives the refused files of a batch, with typed reasons. |
paste | boolean | true | Also accept files pasted from the clipboard while focus is inside the drop surface. |
preventWindowDrop | boolean | true | Keep a document-level guard armed so a missed FILE drop never navigates the browser away. The guard is shared and ref-counted across every mounted hook on the page (it stays armed while ANY instance wants it), and it is payload-scoped: only drags carrying files are cancelled — text dragged into an unrelated input keeps working. |
ref | React.Ref<HTMLDivElement> | — | Ref forwarded to the drop surface (data-slot="dropzone"). |
Data attributes and CSS variables on Dropzone
| Attribute | Values |
|---|---|
data-disabled | "" |
data-drag-invalid | "" |
data-dragging | "" |
data-slot | "dropzone" |
Rejection shape
| Prop | Type | Default | Description |
|---|---|---|---|
file* | File | — | The refused file. |
reasons* | FileDropRejectionReason[] | — | Every reason that applied. |
Accessibility
- The accessible control is the drop surface itself —
role="button", named viaaria-label,tabIndex=0, with Enter/Space opening the picker through the engine's keyboard handler. The real<input type="file">behind it is the display:none form/picker bridge, deliberately not a tab stop (an interactive control may not nest another). - Accepted and refused batches announce through a polite live region ("Added shot.png", "2 files were refused — too large").
- The surface reflects drag state as
data-dragging/data-drag-invalid(primary vs destructive border tint) — state is never colour alone: the refusal is also announced and reported throughonFilesRejected. - A missed drop never navigates the browser away — a document-level guard,
scoped to file-bearing drags only (text dragged into your other inputs
keeps working) and shared page-wide with ref-counting: it stays armed
while any mounted Dropzone wants it, and disarms when the last one
unmounts. Opt a surface out with
preventWindowDrop.
| Key | Action |
|---|---|
| Tab | Focus the drop surface. |
| Enter / Space | Open the file browser. |
| ⌘+V | Paste files while inside the surface. |
| Contract | States tested |
|---|---|
| Behaviour | idle, dragging, drag-invalid, accepted, rejected, disabled |
| Accessibility | keyboard, labeled, status-announcement, semantic-html |
| Visual | default, dragging, drag-invalid, disabled |