Skip to main content

FileInput

Picks files, and hands them over as Files.

Reading them is yours: an audio file goes to decodeAudioData, a preset to JSON.parse. The component stops at the point where the browser has handed over the bytes.

Source Style
Loading...
function App() {
  const [files, setFiles] = useState<File[]>([])
  const [rejected, setRejected] = useState<File[]>([])

  return (
    <div>
      <FileInput.Root
        className={fileInputTheme.root}
        accept="audio/*"
        multiple
        onChange={(files) => {
          setRejected([])
          setFiles(files)
        }}
        onReject={(files) => setRejected(files)}
      >
        <FileInput.Trigger className={fileInputTheme.trigger}>
          Choose audio
        </FileInput.Trigger>
        <span>{files.length > 0 ? `${files.length} selected` : 'No file'}</span>
      </FileInput.Root>
      <ul>
        {files.map((file) => (
          <li key={file.name}>
            {file.name} ({file.type || 'unknown type'})
          </li>
        ))}
      </ul>
      {rejected.length > 0 && (
        <p>Not audio: {rejected.map((file) => file.name).join(', ')}</p>
      )}
    </div>
  )
}

Import​

import { FileInput } from '@tremolo-ui/react'

What it handles for you​

accept is checked again on the way in. The browser treats the attribute as a hint to the picker: a person can switch it to "All Files" and choose anything. Whatever does not match arrives at onReject rather than onChange, so the reason can be shown instead of a silent no-op.

Picking the same file twice works. A file input fires no change event when the selection has not changed, so re-picking the file you just picked would do nothing. The value is cleared as soon as it has been read.

The native input is still the control. It is in the tree, out of sight but in the tab order, so it takes the focus, opens the picker on Enter, and carries the accessible name. FileInput.Trigger is a <label> for it, which is why a click reaches the picker with no JavaScript of its own.

Since the focus lands on something invisible, draw the focus ring from the parent — :focus-within on .root is what the demo theme does.

The ARIA attributes of the input are taken by Root rather than left on the wrapper it draws, so they reach the control: aria-describedby points a hint like "WAV or AIFF, up to 10 MB" at it, and aria-label names it where there is no FileInput.Trigger to take the name from.

Receiving a drop as well​

FileInput answers a click. To take a file dropped onto the page, see DropZone, which reports the same File[].

API Reference​

Each part lists the props of its own. It also takes the attributes of the element it renders, such as className and style.

In Svelte and Vue the props are the same, written their way — class rather than className, bind:value or v-model, snippets or slots for content. See Svelte and Vue.

State is on the element as a data attribute, so it is a selector away — see Styling. An attribute is only there while its state is on.

Root​

Holds the settings and the hidden <input type="file">, and reports what was picked.

PropTypeDefault
acceptstring—
multiplebooleanfalse
disabledbooleanfalse
onChange(files: File[]) => void—
onReject(files: File[]) => void—
children*ReactNode—

Full reference: FileInputProps

Data attributeValues
[data-disabled]Present when disabled is set

Trigger​

Opens the picker, and names the input for a screen reader. Rendered as a <label>.

PropTypeDefault
children*ReactNode—

Full reference: FileInputTriggerProps

Data attributeValues
[data-disabled]Present when disabled is set on Root