File inputs
Single file →
File | null. Multiple →readonly File[]. Live handles, ready forFormDataor your upload pipeline.
- Category
- Directive binding
- Element
<input type="file"> · <input type="file" multiple>- Modifiers
- none
- Leaf types
File | null (single) · readonly File[] (multiple)
Pick a file in the single input to watch the avatar slot fill in with the file name + size. Pick more in the multi-input below to see them stack; the directive writes a File[] in the order the picker returned them. Both surfaces hand you live File handles you can append to FormData or stream into an upload.
Single file → File | null
<input v-register="form.register('avatar')" type="file" accept="image/*" />
const form = useForm({
schema: z.object({
avatar: z.file().nullable(),
}),
defaultValues: { avatar: null },
})
form.values.avatar // File | null
Selecting a file writes the File handle into storage. Clearing the input (or selecting then cancelling) writes null.
Multiple files → File
<input v-register="form.register('attachments')" type="file" multiple />
const form = useForm({
schema: z.object({
attachments: z.array(z.file()),
}),
defaultValues: { attachments: [] },
})
form.values.attachments // readonly File[]
Every file in the picker selection lands in the array, in picker order. Re-selecting replaces the array; the input never accumulates across picks.
Zod v3, and z.instanceof(File)
z.file() is a Zod v4 kind. On Zod v3 the spelling is z.instanceof(File), which works identically through the directive and is also accepted by the v4 adapter, so a schema written this way runs unchanged on both majors:
const schema = z.object({
avatar: z.instanceof(File).nullable(),
attachments: z.array(z.instanceof(File)),
})
The trade-off is refinements. z.file() understands .min(size), .max(size), and .mime([...]); z.instanceof(File) is an opaque leaf, so size and type rules go in a .refine(...) you write yourself. Prefer z.file() when you are on v4 and not sharing the schema with a v3 codebase.
Uploading via FormData
Live File handles work directly with the upload pipelines you'd reach for anyway:
const onSubmit = form.handleSubmit(async (values) => {
const body = new FormData()
if (values.avatar) body.append('avatar', values.avatar)
for (const file of values.attachments) {
body.append('attachments', file)
}
await fetch('/api/upload', { method: 'POST', body })
})
The handles Attaform gives back are the originals, not copies, so they append straight to FormData. The schema's z.file() leaf type carries the constraint through validation; refinements like .min(size), .max(size), .mime([...]) surface in form.errors.<path> like any other validator.
Reset behavior
form.reset() clears the file input back to null / []. The directive also resets the underlying <input>'s value attribute, so the picker shows "No file chosen" after the reset, no stale filename hanging around.
Where to next
reset&resetField: programmatic clearing.- Schema-driven coercion: how File leaves move through the directive.
handleSubmit: wraps your upload callback in the validation gate.