react-hook-form-bound multi-file drag-and-drop upload field. Compound component: Dropzone is the drop area and Dropzone.List renders the uploaded files. Both wrap the @uxf/ui/dropzone primitive and manage its value, isInvalid, and helperText from form state.
Use @uxf/form/dropzone for uploading one or more files via drag-and-drop or a click-to-browse area inside a @uxf/form form — it registers the field with react-hook-form via control/name, adds file-count validation (minFilesCount / maxFilesCount), and renders errors automatically. For a standalone, manually-controlled dropzone (outside a form), use the @uxf/ui/dropzone twin directly. For a single-file field use @uxf/form/file-input; for an avatar/image preview use @uxf/form/avatar-file-input.
Render Dropzone (the drop area) and Dropzone.List (the file list) against the same control and name so they share the field value:
import { Form } from "@uxf/form/form";
import { Dropzone, DropzoneValue } from "@uxf/form/dropzone";
import { FileResponse } from "@uxf/core/types";
import { getDropzoneState } from "@uxf/ui/utils/get-dropzone-state";
import { handleRejectedFiles } from "@uxf/ui/dropzone/handle-rejected-files";
import { useForm } from "react-hook-form";
// your uploader: send the file to storage, resolve with the stored file
declare function uploadFile(file: File): Promise<FileResponse>;
interface FormData {
attachments: DropzoneValue;
}
function Example() {
const formApi = useForm<FormData>({ defaultValues: { attachments: undefined } });
const { status } = getDropzoneState(formApi.watch("attachments"));
return (
<Form formApi={formApi} id="example" onSubmit={(values) => console.log(values)}>
<Dropzone
accept={{ "image/png": [".png"] }}
control={formApi.control}
isDisabled={status === "UPLOADING"}
isRequired
label="Use drag and drop or click to upload"
maxFileSize={1024 * 1024}
name="attachments"
onDropRejected={handleRejectedFiles}
onUploadFile={uploadFile}
/>
<Dropzone.List control={formApi.control} name="attachments" />
</Form>
);
}
Value is DropzoneValue = DropzoneFile[] | undefined (DropzoneFile from @uxf/ui/dropzone/types, which extends FileResponse with upload bookkeeping such as progress and originalFile).
import { Dropzone } from "@uxf/form/dropzone" — the compound component (Dropzone = drop area, Dropzone.List = list). Also exports types DropzoneProps, DropzoneListProps, DropzoneValue.import { DropzoneInput } from "@uxf/form/dropzone/dropzone-input" and import { DropzoneList } from "@uxf/form/dropzone/dropzone-list". Prefer the compound Dropzone / Dropzone.List form (used in the stories).Dropzone (drop area) — DropzoneProps<FormData>ControlProps<FormData> + the visual props of the @uxf/ui/dropzone twin (minus the ones this field manages: isFocused, isInvalid, onChange, value) + the field-specific props below.
| Prop | Type | Default | Description |
|---|---|---|---|
control | Control<FormData> | — | Required. The control from useForm. |
name | FieldPath<FormData> | — | Required. Field path in the form values. |
onUploadFile | (file: File, options?) => Promise<FileResponse> | — | Required (from the twin). Uploads a dropped file; resolves to the stored file. |
rules | RegisterOptions | — | Extra react-hook-form rules; merged with the built-in count validation (a validate function is kept and run alongside as custom). |
shouldUnregister | boolean | — | Unregister the field (drop its value) on unmount. |
isRequired | boolean | false | Requires at least minFilesCount ?? 1 file(s). |
minFilesCount | number | — | Minimum number of files. Also drives the required check (set to 0 to disable requiring a file even with isRequired). |
requiredMessage | string | localized | Message for the required check (defaults to uxf-form-dropzone:validation.required). |
onChange | (value: DropzoneFile[] | undefined, event?) => void | — | Called after the field value updates. |
maxFilesCount is a twin prop (forwarded) that additionally drives the max-files validation. All other props (accept, maxFileSize, minFileSize, icon, label, helperText, isNotClickable, isNotDraggable, onDropRejected, onUploadComplete, onUploadError, style, className, id, …) are forwarded to the twin — see @uxf/ui/dropzone.
Dropzone.List (file list) — DropzoneListProps<FormData>Reads the same field (control + name) and renders the uploaded files. It does not take rules, shouldUnregister, or any validation props.
| Prop | Type | Default | Description |
|---|---|---|---|
control | Control<FormData> | — | Required. Same control as the drop area. |
name | FieldPath<FormData> | — | Required. Same field path as the drop area. |
onChange | (value: DropzoneFile[] | undefined, event?) => void | — | Called after a removal updates the field value. |
errorText | string | (twin default) | Text under a file that failed to upload. |
isDownloadableOnClick | boolean | false | Render the file link with download instead of opening in a new tab. |
onRemoveConfirm | (file: DropzoneFile) => Promise<boolean> | — | Confirm before removing; remove only if it resolves true. |
renderItem | (file, onRemove, isUploading) => ReactNode | — | Custom renderer for each list item. |
className / style | string / CSSProperties | — | List styling. |
See @uxf/ui/dropzone for the visual behaviour of both parts.
Built-in validate on the drop area field, ordered:
isRequired and minFilesCount !== 0 and there are fewer than minFilesCount ?? 1 files → requiredMessage / uxf-form-dropzone:validation.required.maxFilesCount is set and the value exceeds it → uxf-form-dropzone:validation.max-files-count (interpolates {{maxFilesCount}}).minFilesCount is set and the value is below it → uxf-form-dropzone:validation.min-files-count (interpolates {{minFilesCount}}).Additional rules are merged; a rules.validate function runs alongside as custom, a rules.validate object is spread in. The field-level error is shown as the twin's helperText and sets isInvalid.
Form — it reads the form context (formId, and inherits isDisabled / isReadOnly) and needs a react-hook-form control. The drop-area id defaults to ${formId}__${name}.Dropzone and Dropzone.List must use the same control and name to stay in sync.@uxf/ui/dropzone, so include that component's stylesheet(s) and the @uxf/ui token layer — see @uxf/ui/dropzone.@uxf/ui/dropzone