Rxova
Skip to content

File input — Usage

Choose files or drop them on the zone. Nothing leaves your browser — the component issues no network request at all. (FileInput and useState are already in scope.)

Editable — try changing it
function Demo() {
  const [files, setFiles] = useState([])
  return (
    <>
      <FileInput label="Attachments" multiple maxFiles={4} value={files} onChange={setFiles} />
      <p>{files.length === 0 ? 'Nothing attached' : `${files.length} attached`}</p>
    </>
  )
}
Terminal window
pnpm add @rxova/react-file-input

Omit value and onChange; the field keeps its own state and a native form submit posts it.

<form onSubmit={handleSubmit}>
<FileInput label="Résumé" name="resume" accept=".pdf,.doc,.docx" />
<button type="submit">Apply</button>
</form>
PropWhat it does
acceptThe native accept grammar: .ext, type/sub, type/*. Lenient when the browser reports no MIME type.
maxSize / minSizeBytes. An inverted range is ignored rather than enforced, and reported through onWarn.
maxFilesIgnored unless multiple. A single-file field is always capped at one.
dedupeOn by default. Identity is name + size + last-modified, like the native control.
validateFinal say. true, false, or a string that becomes the rejection message.

Every refusal reaches onReject once per file, so a selection of five where two fail still adds the other three.

Editable — try changing it
function Demo() {
  const [message, setMessage] = useState('')
  return (
    <>
      <FileInput
        label="Small text files only"
        multiple
        accept=".txt,text/plain"
        maxSize={1000}
        validate={(file) => (file.name.startsWith('secret') ? 'that one stays home' : true)}
        onReject={(attempt) => {
          setMessage(describeRejection(attempt, { accept: '.txt', maxSize: 1000 }))
        }}
      />
      <p role="status">{message}</p>
    </>
  )
}

describeRejection turns any rejection into a sentence; reason is one of type, too-large, too-small, duplicate, max-files or invalid if you would rather write your own.

previews opts into the object-URL lifecycle. URLs are minted only for images, only on the client, and revoked as soon as the file leaves the list.

<FileInput label="Photos" multiple previews accept="image/*" />

It is off by default because a URL nobody revokes is a memory leak, and opting into the lifecycle should be deliberate.

renderFile replaces the painted row; the remove button stays, so a custom renderer cannot accidentally drop the only way to detach a file.

<FileInput
label="Attachments"
multiple
renderFile={({ file, size, preview }) => (
<span>
{preview ? <img src={preview} alt="" width={32} /> : null}
<strong>{file.name}</strong>{size}
</span>
)}
/>

useFileInput gives you the state and the handlers with no markup at all. The fiddly parts — the drag-depth counter, the URL revocation, the focus handoff after a removal — live in the hook.

const field = useFileInput({ multiple: true, previews: true, maxSize: 1_000_000 })
ReturnedWhat it is
files / entriesThe selection, and the same list with stable keys and previews
dragging / fullA file drag is over the zone; the count or single-file cap is hit
open()Opens the native picker
addFiles(list)Applies every rule, then commits what passed
removeAt(i)Removes one file and moves focus
clear()Empties the selection
handle*InputChange, DragEnter, DragOver, DragLeave, Drop, Focus, Blur

No stylesheet ships. Structure only, with data-* hooks covered by semver:

data-rx-file-root, data-rx-file-input, data-rx-file-zone, data-rx-file-list, data-rx-file-file, data-rx-file-name, data-rx-file-size, data-rx-file-preview, data-rx-file-remove, data-rx-file-announcement, plus data-dragging, data-disabled and data-invalid on the root.

[data-rx-file-zone] {
border: 2px dashed;
padding: 2rem;
width: 100%;
}
[data-rx-file-root][data-dragging] [data-rx-file-zone] {
border-color: rebeccapurple;
}
<FileInput
label="Files"
onWarn={(warning) => Sentry.captureMessage(warning.message, { extra: warning })}
/>

Fires when a prop is coerced rather than honoured — max-files-invalid, size-range-invalid, negative-size, accept-suspicious, single-with-max. Deduped per instance, falls back to console.warn, and the entire path is removed from production builds.