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.
- React
- Svelte
- Vue
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> ) }
<script lang="ts">
import { FileInput } from '@tremolo-ui/svelte'
// Copy this file from the Styling page into your own project.
import fileInputTheme from './FileInput.module.css'
let files: File[] = $state([])
let rejected: File[] = $state([])
</script>
<FileInput.Root
class={fileInputTheme.root}
accept="audio/*"
multiple
onChange={(picked) => {
rejected = []
files = picked
}}
onReject={(picked) => (rejected = picked)}
>
<FileInput.Trigger class={fileInputTheme.trigger}
>Choose audio</FileInput.Trigger
>
<span>{files.length > 0 ? `${files.length} selected` : 'No file'}</span>
</FileInput.Root>
<ul>
{#each files as file}
<li>{file.name} ({file.type || 'unknown type'})</li>
{/each}
</ul>
{#if rejected.length > 0}
<p>Not audio: {rejected.map((file) => file.name).join(', ')}</p>
{/if}
<script setup lang="ts">
import { ref } from 'vue'
import { FileInput, FileInputTrigger } from '@tremolo-ui/vue'
// Copy this file from the Styling page into your own project.
import fileInputTheme from './FileInput.module.css'
const files = ref<File[]>([])
const rejected = ref<File[]>([])
function onChange(picked: File[]) {
rejected.value = []
files.value = picked
}
</script>
<template>
<FileInput
:class="fileInputTheme.root"
accept="audio/*"
multiple
@change="onChange"
@reject="(picked) => (rejected = picked)"
>
<FileInputTrigger :class="fileInputTheme.trigger">
Choose audio
</FileInputTrigger>
<span>{{ files.length > 0 ? `${files.length} selected` : 'No file' }}</span>
</FileInput>
<ul>
<li v-for="(file, i) in files" :key="i">
{{ file.name }} ({{ file.type || 'unknown type' }})
</li>
</ul>
<p v-if="rejected.length > 0">
Not audio: {{ rejected.map((file) => file.name).join(', ') }}
</p>
</template>
Import
- React
- Svelte
- Vue
import { FileInput } from '@tremolo-ui/react'
import { FileInput } from '@tremolo-ui/svelte'
import { FileInput, FileInputTrigger } from '@tremolo-ui/vue'
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.
| Prop | Type | Default |
|---|---|---|
accept | string | — |
multiple | boolean | false |
disabled | boolean | false |
onChange | (files: File[]) => void | — |
onReject | (files: File[]) => void | — |
children* | ReactNode | — |
Full reference: FileInputProps
| Data attribute | Values |
|---|---|
[data-disabled] | Present when disabled is set |
Trigger
Opens the picker, and names the input for a screen reader. Rendered as a
<label>.
| Prop | Type | Default |
|---|---|---|
children* | ReactNode | — |
Full reference: FileInputTriggerProps
| Data attribute | Values |
|---|---|
[data-disabled] | Present when disabled is set on Root |