Skip to content

FileDrop

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/file-drop.json
See it liveTaking files, filtering them, and closed.→

It takes files. It does not upload them. Where they go, with which credentials, retried how, resumed or not — that is your transport, and a primitive that owned it would be wrong for every product whose upload does not look like the one it guessed. This is the same boundary Field draws around validation: the component knows the shape of the interaction, you know what the interaction means.

<FileDrop
accept="image/*"
maxSize={5 * 1024 * 1024}
multiple
onFiles={(files) => upload(files)}
onReject={(rejections) => toast(explain(rejections))}
aria-label={t('attachments')}
>
{t('drop-or-choose')}
</FileDrop>

There is a real <input type="file"> underneath, hidden with sr-only rather than display: none. That is not fussiness: hidden the other way it is unfocusable, the label stops reaching it, and the field becomes mouse-only. The native input is also what the operating system’s picker attaches to and what a screen reader announces as a file field.

Rejected files are reported, not swallowed. A file dropped and silently ignored looks like a broken page. onReject hands back each file with a reason — type, size or count — and you turn that into a sentence, in your own language.

Two browser defaults are handled, and both are invisible until they are not. dragover is prevented, without which the browser navigates to the dropped file and the form the reader was filling in is simply gone. And the input’s value is cleared after each change, without which choosing the same file twice in a row fires nothing the second time.

The lit state counts enters and leaves rather than toggling. dragleave fires when the pointer crosses onto a child of the zone, so a zone that toggled on it flickers as the pointer moves over its own text — the commonest defect in hand-written drop zones.

Props

Read from the types in ui/file-drop.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
onFilesrequired(files: File[]) => voidCalled with the files that passed the filters.
onReject(rejections: FileRejection[]) => voidCalled with the ones that did not, so the product can say why.
acceptstringWhat to take, in the form <input accept> uses: image/*, .pdf,.docx.
maxSizenumberLargest file, in bytes.
maxFilesnumberHow many at once. Without it, any number.
multiplebooleanWhether several may be chosen in the picker.
disabledbooleanDims the zone, ignores drops and disables the input underneath.
childrenReactNodeWhat the zone says. The product's words, in the product's language.
classNamestring
idstringThe id of the hidden file input, which the zone's label points at. A generated one if omitted.
aria-labelstringNames the field for a screen reader, when there is no visible label.
aria-labelledbystringThe id of the element that names the field, when it is not the zone's own text.
aria-describedbystringThe id of the element that describes the field - the accepted types, the size limit.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 13 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/file-drop.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.20.0
    • feat five components for the harder half of a form cfb317a