File upload
A recipe, not a package namespace. A native <input type="file"> is the control, useDropzone adds drag and drop and file checks, and your code owns the upload. There is no FileUpload.* export.
Images
The label is the drop target, so a click anywhere on it opens the picker and its text names the input. Rejected files are listed in an ErrorMessage that the input's aria-describedby points to, and the input gets aria-invalid. Accepted files are listed below it.
const zone = useDropzone({
accept: 'image/*',
multiple: true,
maxFiles: 3,
maxSize: 2_000_000,
minSize: 1,
onFiles: (accepted, rejected) => {
setFiles(accepted)
setRejected(rejected)
},
})
<label {...zone.getRootProps()}>
<Text as="span">Drop images here, or choose them</Text>
<input
{...zone.getInputProps()}
name="images"
aria-invalid={rejected.length > 0 || undefined}
aria-describedby={rejected.length > 0 ? `${hintId} ${errorId}` : hintId}
/>
</label>
<Description id={hintId}>Up to 3 images, 2 MB each.</Description>
{rejected.length > 0 ? <ErrorMessage id={errorId}>…</ErrorMessage> : null}
Style the drop state with [data-dragging] or the dragging value. Errors are not announced when they appear; use useAnnouncer if they need to be.
Uploading
Send the files from onFiles, or leave them in the input and let the form submit them: the input holds only the accepted files. Show progress with a native <progress>, as on the Progress page.