---
title: "Binding inputs"
description: "Connect text inputs, textareas, numbers, checkboxes, radios, selects, dates, and files to NotForm fields."
canonical_url: "https://notformdocs.vercel.app/docs/guides/binding-inputs"
---

> Index of every page on this site: [llms.txt](https://notformdocs.vercel.app/llms.txt)

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](https://notformdocs.vercel.app/docs/integrations/custom-components).

## The pattern

```vue
<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.model` is a two-way binding to `form.values` at the field's path. Writing to it does **not** validate.
- `field.events` is `{ onBlur, onChange, onInput }`. These mark the field touched and trigger validation according to the field's configuration.
- Put `v-model` before `v-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.

```ts
const form = useForm({
  initialValues: {
    age: undefined,
    interests: [],
    name: '',
    newsletter: false
  },
  schema,
})
```

## Text, email, password, search, URL

```vue
<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

```vue
<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

```vue
<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>
```

```ts
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](https://notformdocs.vercel.app/docs/guides/schema-input-output).

## Single checkbox

Bind a boolean.

```vue
<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>
```

```ts
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`.

```vue
<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>
```

```ts
const schema = z.object({
  interests: z.array(z.string()).min(1, 'Pick at least one.'),
})
// initialValues: { interests: [] }
```

<note>

A checkbox group is a plain `form.field()` whose value happens to be an array of fixed choices. Use [`form.arrayField()`](https://notformdocs.vercel.app/docs/guides/array-fields) when users add, remove, or reorder rows of structured data.

</note>

## Radio group

All radios share one field and differ by `:value`.

```vue
<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

```vue
<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.

```vue
<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.

```vue
<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.

```vue
<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.path` for the control's `id` and the label's `for`. `field.id` is 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](https://notformdocs.vercel.app/docs/guides/errors#accessible-error-markup).

## Next steps

- [Validation](https://notformdocs.vercel.app/docs/guides/validation) controls when errors appear.
- [Errors](https://notformdocs.vercel.app/docs/guides/errors) covers rendering every kind of error.

## Sitemap

See the full [sitemap](https://notformdocs.vercel.app/sitemap.md) for all pages.
