Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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
stable
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/dropzone

The 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

BehaviourWhere it lives
Upload lifecycle, progressHost + Attachment's state machine
Drop/paste/browse behaviouruse-file-drop — compose it directly for chrome-less surfaces (a rich-text composer accepting pasted images)
The dashed idle affordanceChildren — compose Empty variant="dashed", "the classic drop-zone look"
Rendering staged filesHost, 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

A non-Empty child still shows the drag state — the stroke belongs to the surface.

API Reference

PropTypeDefaultDescription
children*React.ReactNodeThe idle affordance — typically Empty variant="dashed" content.
onFilesAccepted*(files: File[]) => voidReceives the accepted files of each drop/paste/browse batch.
acceptAcceptAccepted types, MIME-to-extensions (react-dropzone's Accept shape): { "image/*": [".png", ".jpg"] }.
aria-labelstring"Upload files"Accessible name for the drop surface (the focusable control).
classNamestringExtra classes for the drop surface.
disabledbooleanfalseDisable 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.
maxFilesnumberMaximum files per batch.
maxSizenumberPer-file maximum size in bytes.
minSizenumberPer-file minimum size in bytes.
multiplebooleantrueAllow more than one file per batch.
onFilesRejected((rejections: FileDropRejection[]) => void)Receives the refused files of a batch, with typed reasons.
pastebooleantrueAlso accept files pasted from the clipboard while focus is inside the drop surface.
preventWindowDropbooleantrueKeep 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.
refReact.Ref<HTMLDivElement>Ref forwarded to the drop surface (data-slot="dropzone").

Data attributes and CSS variables on Dropzone

AttributeValues
data-disabled""
data-drag-invalid""
data-dragging""
data-slot"dropzone"

Rejection shape

PropTypeDefaultDescription
file*FileThe refused file.
reasons*FileDropRejectionReason[]Every reason that applied.

Accessibility

  • The accessible control is the drop surface itselfrole="button", named via aria-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 through onFilesRejected.
  • 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.
KeyAction
TabFocus the drop surface.
Enter / SpaceOpen the file browser.
+VPaste files while inside the surface.
ContractStates tested
Behaviouridle, dragging, drag-invalid, accepted, rejected, disabled
Accessibilitykeyboard, labeled, status-announcement, semantic-html
Visualdefault, dragging, drag-invalid, disabled

Do / Don't

Do
Meet Attachment at a plain File[] — stage files in your state and render their upload lifecycle with Attachment's state machine.
Don't
Ask Dropzone to render attachments or own uploads — acquisition and upload state are separate jobs on purpose.

On this page