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