File Upload
A drop target and a picker trigger, because a file input cannot be styled.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/file-upload.jsonUsage
import { FileDropzone, FileTrigger } from "@/components/ui/file-upload"Two components over the same hidden input. FileDropzone is the bordered area; FileTrigger
turns a control you already have into a picker.
A trigger on your own control
<FileTrigger
accept="application/pdf"
onSelect={(files) => importPdf(files[0])}
render={<Button variant="secondary" size="sm" />}
>
<PaperclipIcon />
Choose file
</FileTrigger>onSelect receives a File[] and is never called with an empty one, so there is no cancelled-
picker case to handle.
Picking the same file twice works. A file input fires change only when its value
changes, so re-picking the file you just picked is normally silent — which reads as the upload
having failed. The input is cleared after every selection, so the second pick fires like the
first.
While it uploads
Disable the zone while a transfer is running. A target that still highlights during an upload invites a second file it is going to drop on the floor.
Composition
The dropzone's contents are yours; three class names are provided so they take the right type and colour, including when the zone is disabled.
<FileDropzone accept=".dcm,.zip" multiple onSelect={setFiles}>
<CloudUploadIcon className="cn-file-dropzone-icon size-7" />
<span className="cn-file-dropzone-label">Click or drop to upload</span>
<span className="cn-file-dropzone-hint">DICOM or ZIP · up to 200 MB</span>
</FileDropzone>Accessibility
The dropzone is a real button. Dragging cannot be done from a keyboard, cannot be done on a touch screen, and gives no on-screen sign that it is possible. Dropping is added on top of a control that works without it — so Tab reaches it, Enter and Space open the picker, and the focus ring is the same brand border as hover.
The input is clipped, not hidden. display: none and visibility: hidden both take it out
of the accessibility tree, which leaves a keyboard user with a button that opens a dialog they
were never told about. It is clipped to one pixel instead, so it stays announced.
accept is a filter on the picker, not a guarantee. A file can still arrive by drop with any
type at all, so validate on selection and again on the server — never treat the extension as
proof of what a file is.
API Reference
Both components take the same props.
Prop
Type