Dropzone
A drag-and-drop (or click) upload area wired to useFiles - uploads through your gateway, with composable success, prompt and error states.
A compound dropzone that uploads through a useFiles instance. Drag files in or click to open the picker. With no prefix (and outside folder uploads), the server mints each key and the bytes go straight to storage on a presigned URL, or through your gateway when the adapter can’t sign. With a prefix or a folder path, each file gets an explicit key and streams through the gateway.
"use client";
import { demoFiles } from "@/lib/demo-files";
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
DropzoneError,
} from "@/registry/files-sdk/dropzone/dropzone";
const Example = () => {
const files = demoFiles;
return (
<Dropzone accept="image/*" files={files} maxFiles={3} prefix="demo/">
<DropzoneContent />
<DropzoneEmptyState />
<DropzoneError />
</Dropzone>
);
};
export default Example;
Installation
npx shadcn@latest add https://files-sdk.dev/r/dropzone.jsonUsage
import { useFiles } from "files-sdk/react";
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
DropzoneError,
} from "@/components/files-sdk/dropzone";
export function Uploader() {
const files = useFiles({ endpoint: "/api/files" });
return (
<Dropzone accept="image/*" files={files} prefix="docs/" maxFiles={5}>
<DropzoneContent />
<DropzoneEmptyState />
<DropzoneError />
</Dropzone>
);
}
DropzoneEmptyState renders the prompt — and keeps rendering it after uploads, so users can keep adding files. DropzoneContent shows a success summary (“3 files uploaded”) once something lands, and DropzoneError surfaces the latest batch’s failures (files that don’t match accept, oversize files, files over the maxFiles limit, and failed uploads) until the next upload starts on the same useFiles() instance, including one your page starts itself, such as a retry. Pass your own children to any of them to override the default look (the zone is a <button>, so keep them to phrasing content such as spans, icons and text), or wire onUploaded / onError to render state outside the dropzone entirely.
accept filters the file picker and is enforced on dropped files too, using the same rules: .ext suffixes, type/* wildcards, and exact MIME types.
Folder uploads
Set directory to accept whole folders. The click-to-browse picker becomes a directory picker, dropped folders are traversed recursively, and each file keeps its relative path in the key: dropping my-skill/ with prefix="skills/" uploads skills/my-skill/references/api-guide.md, ready for a tree view in FileBrowser.
"use client";
import { demoFiles } from "@/lib/demo-files";
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
DropzoneError,
} from "@/registry/files-sdk/dropzone/dropzone";
const Example = () => {
const files = demoFiles;
return (
<Dropzone directory files={files} prefix="skills/">
<DropzoneContent />
<DropzoneEmptyState />
<DropzoneError />
</Dropzone>
);
};
export default Example;
To let users pick the destination themselves, drive prefix from an input:
import { useState } from "react";
import { useFiles } from "files-sdk/react";
import { Input } from "@/components/ui/input";
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
DropzoneError,
} from "@/components/files-sdk/dropzone";
export function FolderUploader() {
const files = useFiles({ endpoint: "/api/files" });
const [destination, setDestination] = useState("references/");
return (
<div className="flex flex-col gap-2">
<Input
aria-label="Destination folder"
onChange={(event) => setDestination(event.target.value)}
value={destination}
/>
<Dropzone directory files={files} prefix={destination}>
<DropzoneContent />
<DropzoneEmptyState />
<DropzoneError />
</Dropzone>
</div>
);
}
With directory set, maxFiles defaults to unlimited; pass a number to cap the total file count per drop.
Accessibility
The zone is a native <button>, so it’s in the tab order and Enter or Space opens the file picker. While uploads run it’s marked aria-disabled and ignores activation, but stays focusable.
A button’s content is its accessible name, so the dropzone keeps the summaries out of it. The name is the prompt from DropzoneEmptyState (or your own children). DropzoneContent and DropzoneError are hidden from assistive tech inside the button and referenced as its description instead, so a screen reader reads the prompt and its limits as the name, then the latest outcome. Errors are also announced when they happen: the dropzone mirrors the failure summary into a visually hidden, polite live region beside the button, and a repeat of the same error is announced again. Custom DropzoneError children are visual only; the announcement uses the summary text.
Successful uploads aren’t announced by the dropzone. Pair it with Upload Progress, which announces each file as it finishes or fails, or announce from onUploaded. With both on the page, a failed upload is announced twice: once for the file by Upload Progress, and once in the dropzone’s batch summary.
Props
<Dropzone>
filesUseFilesResult
A `useFiles()` instance — the dropzone uploads through it.
UseFilesResultprefix?string
Key prefix (folder) for explicit keys, e.g. `"docs/"`. Empty = server mints the key.
stringaccept?string
Accepted file types, e.g. `"image/*"` or `".pdf,.docx"` — the file input's `accept` filter, also enforced on dropped files (non-matching files are reported as failed).
stringdirectory?boolean
Accept whole folders: the picker selects a directory and dropped folders are traversed recursively, with relative paths preserved in keys (`prefix + folder/sub/file.ext`).
booleanmaxFiles?number
Max files per drop. Default 1, or unlimited when `directory` is set.
numbermaxSize?number
Max bytes per file; larger files are reported as failed.
numberonUploaded?(entry: UploadedEntry) => void
Called after each successful upload.
(entry: UploadedEntry) => voidonError?(error: Error, file: File) => void
Called for each file that fails to upload or is rejected client-side.
(error: Error, file: File) => voidclassName?string
stringchildren?ReactNode
ReactNode<DropzoneContent>, <DropzoneEmptyState> and <DropzoneError> each accept className and children to override the default look.