Binding inputs
Every control connects to a field the same way: write the value through field.model, and report interaction through field.events. This page shows the pattern for each native input type. For custom components, see Custom components.
The pattern
<template>
<Field
v-slot="{ field }"
:field="form.field('email')"
>
<label :for="field.path">Email</label>
<input
:id="field.path"
v-model="field.model"
v-bind="field.events"
autocomplete="email"
type="email"
>
</Field>
</template>
field.modelis a two-way binding toform.valuesat the field's path. Writing to it does not validate.field.eventsis{ onBlur, onChange, onInput }. These mark the field touched and trigger validation according to the field's configuration.- Put
v-modelbeforev-bind="field.events". The value must be written before the validation event reads it.
Always give initial values
Though not mandatory, provide an entry in initialValues for every field you bind. Missing values reach your schema as undefined, so validators report their generic "expected string, received undefined" message instead of your custom one, and isDirty can become true after a user types and deletes.
const form = useForm({
initialValues: {
age: undefined,
interests: [],
name: '',
newsletter: false
},
schema,
})
Text, email, password, search, URL
<template>
<Field
v-slot="{ field }"
:field="form.field('name')"
>
<label :for="field.path">Name</label>
<input
:id="field.path"
v-model="field.model"
v-bind="field.events"
type="text"
>
<FieldError :errors="field" />
</Field>
</template>
Native modifiers work as usual. v-model.trim for example removes surrounding whitespace as the user types.
Textarea
<template>
<Field
v-slot="{ field }"
:field="form.field('message')"
>
<label :for="field.path">Message</label>
<textarea
:id="field.path"
v-model="field.model"
v-bind="field.events"
rows="4"
/>
</Field>
</template>
Number
<template>
<Field
v-slot="{ field }"
:field="form.field('quantity')"
>
<label :for="field.path">Quantity</label>
<input
:id="field.path"
v-model.number="field.model"
v-bind="field.events"
type="number"
min="0"
>
</Field>
</template>
const schema = z.object({ quantity: z.number().min(1, 'Order at least one.') })
// initialValues: { quantity: 1 }
v-model.number converts typed text to a number. If the user clears the box, the browser reports an empty string, which a z.number() schema rejects with its type message. Set a custom type message in your schema, or keep the input as a string and convert in the schema. See Schema input and output.
Single checkbox
Bind a boolean.
<template>
<Field
v-slot="{ field }"
:field="form.field('acceptTerms')"
>
<label>
<input
v-model="field.model"
v-bind="field.events"
type="checkbox"
>
I accept the terms
</label>
<FieldError :errors="field" />
</Field>
</template>
const schema = z.object({
acceptTerms: z.boolean('You must accept the terms.'),
})
// initialValues: { acceptTerms: false }
Checkbox group
Bind one field to an array of the selected values. Each checkbox has its own :value.
<template>
<Field
v-slot="{ field }"
:field="form.field('interests')"
>
<fieldset>
<legend>Interests</legend>
<label
v-for="option in ['design', 'code', 'writing']"
:key="option"
>
<input
v-model="field.model"
v-bind="field.events"
type="checkbox"
:value="option"
>
{{ option }}
</label>
</fieldset>
<FieldError :errors="field" />
</Field>
</template>
const schema = z.object({
interests: z.array(z.string()).min(1, 'Pick at least one.'),
})
// initialValues: { interests: [] }
form.field() whose value happens to be an array of fixed choices. Use form.arrayField() when users add, remove, or reorder rows of structured data.Radio group
All radios share one field and differ by :value.
<template>
<Field
v-slot="{ field }"
:field="form.field('plan')"
>
<fieldset>
<legend>Plan</legend>
<label
v-for="option in ['free', 'pro']"
:key="option"
>
<input
v-model="field.model"
v-bind="field.events"
type="radio"
:value="option"
>
{{ option }}
</label>
</fieldset>
<FieldError :errors="field" />
</Field>
</template>
Select
<template>
<Field
v-slot="{ field }"
:field="form.field('nationality')"
>
<label :for="field.path">Nationality</label>
<select
:id="field.path"
v-model="field.model"
v-bind="field.events"
>
<option
value=""
disabled
>
Choose a country
</option>
<option
v-for="country in countries"
:key="country.code"
:value="country.code"
>
{{ country.name }}
</option>
</select>
<FieldError :errors="field" />
</Field>
</template>
For <select multiple>, bind an array exactly like a checkbox group.
Dates and times
Native date and time inputs produce strings such as 2026-11-02. Bind them like text, and validate or transform them in the schema.
<template>
<Field
v-slot="{ field }"
:field="form.field('birthday')"
>
<label :for="field.path">Birthday</label>
<input
:id="field.path"
v-model="field.model"
v-bind="field.events"
type="date"
>
<FieldError :errors="field" />
</Field>
</template>
Files
v-model does not work on file inputs. Write the file with an event handler instead and report the change yourself.
<template>
<Field
v-slot="{ field }"
:field="form.field('avatar')"
>
<label :for="field.path">Avatar</label>
<input
:id="field.path"
type="file"
@change="(event) => {
field.model = (event.target as HTMLInputElement).files?.[0]
field.events.onChange()
}"
@blur="field.events.onBlur"
>
<FieldError :errors="field" />
</Field>
</template>
Skipping the Field component
Field only hands the field to its slot. If you prefer, create the field in script and use it directly. This is common in reusable input components.
<script setup lang="ts">
const email = form.field('email')
</script>
<template>
<input
v-bind="email.events"
:id="email.path"
v-model="email.model"
type="email"
:name="email.path"
>
</template>
Labels and accessibility
- Use
field.pathfor the control'sidand the label'sfor.field.idis a symbol and is not valid as an HTML id. - Mark invalid controls with
:aria-invalid="!field.isValid". - Link errors to controls with
aria-describedby. See Errors.
Next steps
- Validation controls when errors appear.
- Errors covers rendering every kind of error.
Core concepts
The mental model behind NotForm: the schema, the form instance, fields, paths, the initial-values baseline, and what happens on every keystroke and submit.
Schema input and output
Understand how NotForm types the values users edit separately from the validated data your submit handler receives.