4 FileInput
L4stIdi0t edited this page 2026-10-09 22:01:06 +02:00

FileInput

Picking files, in the two shapes a form actually wants: svelte <FileInput bind:files variant="line" accept=".csv" /> a control the size of an Input, for a field that sits in a column with other fields <FileInput bind:files multiple /> a drop area, for when choosing files IS the screen Both take a drop, both open the picker on click, and both are one real <input type="file"> underneath — so Enter and Space open the picker, the file dialog is the operating system's, and a name submits with the form.

It also writes the list back into that input after every change, which is the part hand-rolled uploaders miss: remove a file from a native input and the form still submits it, because files was never touched. Validation is accept, maxSize and maxFiles, plus a duplicate check — a file dropped twice is dropped once. Whatever is turned away is said out loud, listed under the control, and passed to onReject; nothing is rejected silently.

Forms · two-way: bind:files · submits in a form with name · keyboard contract

<script lang="ts">
	import { FileInput } from '@l4st/driftwood';
</script>
Light Dark
file-input file-input in dark mode

Props

Prop Type Default What it does
files bind: File[] [] The chosen files. Bindable — removing from it updates the control.
variant Variant 'drop' drop is the dashed area; line is the height of an Input.
accept string — Native accept syntax: .csv, image/*, application/pdf, or a mix.
multiple boolean false Allow more than one, and append rather than replace on each pick.
maxSize number — Largest single file, in bytes. Anything over is rejected by name.
maxFiles number — How many files may be held at once.
disabled boolean false Blocks the picker and the drop target.
name string — Submits with the form, exactly what the list shows.
label string — The call to action, e.g. "Drop a backup here".
hint string — One line under it — the place for the accepted types and the size cap.
showList boolean true Render the chosen files. Turn it off to drive your own FileList.
onFiles (files: File[]) => void — The chosen files, after validation. Fires on every accepted pick.
onReject (rejections: FileRejection[]) => void — Everything turned away, and why.
onRemove (file: File, index: number) => void — The removed file and the index it held.
class string — Merged with the component's own classes, last — so yours win.
aria-label string — Names the control when label is not the whole story.
aria-describedby string — Id of the element that names it — what Field passes.
id string —

Keyboard

Key Does
Tab reach the picker — the whole zone is one stop, then one per chosen file
Enter / Space open the file dialog
Cmd/Ctrl + V add files from the clipboard, while the picker has focus

Example

The markup behind the screenshot above, lifted from src/routes/shots/ — so it compiles, and it is exactly what produced the image.

<FileInput
	files={chosen}
	multiple
	hint="Archives and manifests, up to 500 MB each."
	aria-label="Backups to restore"
/>

Notes

The two shapes

variant="line" is the height of an Input, so it sits in a column of form fields without shouting. variant="drop" (the default) is the dashed area, for a screen whose job is choosing files. Both accept a drop; the line one just does not advertise it until you drag something over it.

Line Drop
FileInput, line variant FileInput, drop variant

What the form submits

A native file input keeps whatever the picker handed it. Remove a file from a hand-rolled list and the input is untouched — so the form still uploads the file the user just took out, and nothing anywhere reports a problem.

FileInput assigns the edited set back into its own input after every add and every removal, so the two never disagree. pnpm probe asserts exactly that: pick two, submit, remove one, submit again, and read what FormData actually carried.

That is also why removal is built in rather than left to onRemove — the callback tells you a file went, it is not how it goes.

Validation

accept, maxSize, maxFiles and a duplicate check, applied in that order. Nothing is refused silently: each rejection is listed under the control naming the file and the rule, announced through a live region, and handed to onReject as a FileRejection.

type FileRejection = {
	file: File;
	reason: 'type' | 'size' | 'count' | 'duplicate';
	message: string;
};

Two files count as the same file when their name and size match. Not lastModified: the same file arriving from a different source often carries a freshly stamped mtime, and keying on it lets the obvious duplicate through.

Uploading is yours

The kit holds files and draws progress it is given; it does not know your endpoint. When bytes start moving, turn off the built-in list and drive FileList yourself:

<FileInput bind:files showList={false} onFiles={start} />
<FileList items={uploads} onRemove={(i) => cancel(uploads[i])} />

Forms · Field · Input · FileList · Textarea · CodeEditor · Combobox · RadioGroup · NumberInput · InlineEdit · Label · Select · MultiSelect · DatePicker · TagsInput · Checkbox · Switch · Slider


← All components · Getting started · Theming · source