FileDropField
A real file input dressed as a drop target: an sr-only input inside a dashed box that takes the focus ring, the click and the drop. Two applications had hand-rolled this and had already drifted on the box size and the icon, which is why both are props.
Import
import { FileDropField } from "@mikenotthepope/substrateui"It is an input, not a div with a handler
The dashed box is a <label> and the thing inside it is a real <input type="file">, hidden with sr-only rather than removed. That is what keeps Tab, Space, the platform file picker and form submission working without a line of code, and why the focus ring is a focus-within:ring-2 on the box — a ring drawn around a 1px input is a ring nobody sees.
The box's own text is the input's accessible name. Once that is more than the prompt — a hint, a picked file name — give the field an aria-label of its own, as every example here does.
Size and icon
Both are props because both are what the two hand-rolled copies had already drifted on. size moves the padding, the type scale and the icon together; sizing them separately is how they came apart in the first place.
Reading the files
onFilesChange covers both ways a file arrives, and hands you what survived accept and multiple. A drop does not fire the input's own change event: the dropped files are assigned to the input where the browser allows it, so a plain form submission carries them, but nothing synthesises a change. If a form library is bound to onChange, wire onFilesChange as well or the drop goes unseen.
The browser applies accept to the file picker and to nothing else, so a drop normally arrives unfiltered. This component applies the same list to a drop by hand — extensions, media types and wildcard subtypes like image/*— because a box that says "PDF only" and then swallows anything is worse than one that never offered to take a drop.
Rejected
invalid swaps the border, the fill and the text to the error tokens — the same three Alert's error variant uses — and sets aria-invalid. Inside a Fieldthere is nothing to pass: the field's own error state reaches the box.
Accessibility
The input keeps its own semantics, so a screen reader announces a file input and the keyboard operates it. It is sr-only, not aria-hidden and not tabIndex={-1} — the opposite of HoneypotField, which hides a field from everyone on purpose.
The rejected state is a border colour and aria-invalid, never colour alone.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "default" | Box padding, type scale and icon size, moved together. |
icon | React.ReactNode | <Upload /> | The mark above the prompt. `null` for no icon at all. |
prompt | React.ReactNode | "Choose a file or drag it here" | Replaces the default prompt line. |
hint | React.ReactNode | — | A muted line under the prompt — accepted types, a size limit. |
invalid | boolean | a surrounding Field's error | Draw the box as rejected and set `aria-invalid`. |
onFilesChange | (files: File[]) => void | — | The files after a pick or a drop, already filtered by `accept` and `multiple`. The only event a drop raises. |
labels | FileDropFieldLabels | — | Translations for the prompt and the multi-file summary. |
...input props | React.ComponentProps<'input'> | — | Forwarded to the input — `name`, `accept`, `multiple`, `required`, `disabled`, `aria-label`. `type` is fixed at file. |