Skip to content
Forms

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

PropTypeDefaultDescription
size"sm" | "default" | "lg""default"Box padding, type scale and icon size, moved together.
iconReact.ReactNode<Upload />The mark above the prompt. `null` for no icon at all.
promptReact.ReactNode"Choose a file or drag it here"Replaces the default prompt line.
hintReact.ReactNode—A muted line under the prompt — accepted types, a size limit.
invalidbooleana surrounding Field's errorDraw 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.
labelsFileDropFieldLabels—Translations for the prompt and the multi-file summary.
...input propsReact.ComponentProps<'input'>—Forwarded to the input — `name`, `accept`, `multiple`, `required`, `disabled`, `aria-label`. `type` is fixed at file.