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}
Up to 3 images, 2 MB each.

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.