---
title: "Custom components"
description: "Connect any Vue input component to a NotForm field by mapping its value and events, and build reusable field wrappers."
canonical_url: "https://notformdocs.vercel.app/docs/integrations/custom-components"
---

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

Any control can work with NotForm. You only need to answer two questions about it:

1. **How does it take and emit its value?** Usually `v-model`.
2. **Which of its events mean "typed", "committed", and "left"?** These map to `onInput`, `onChange`, and `onBlur`.

## Choose the binding

<table>
<thead>
  <tr>
    <th>
      The control...
    </th>
    
    <th>
      Bind it like this
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Forwards native <code>
        input
      </code>
      
      , <code>
        change
      </code>
      
      , and <code>
        blur
      </code>
      
       events
    </td>
    
    <td>
      <code>
        v-model="field.model" v-bind="field.events"
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Emits only <code>
        update:modelValue
      </code>
      
       when a value is picked
    </td>
    
    <td>
      <code>
        v-model="field.model" @update:model-value="field.events.onChange"
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Emits <code>
        update:modelValue
      </code>
      
       on every keystroke
    </td>
    
    <td>
      <code>
        v-model="field.model" @update:model-value="field.events.onInput"
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Emits an event when focus leaves, such as <code>
        blur
      </code>
      
      , <code>
        focusout
      </code>
      
      , or <code>
        close
      </code>
    </td>
    
    <td>
      <code>
        @blur="field.events.onBlur"
      </code>
    </td>
  </tr>
</tbody>
</table>

Use the event names from the component's own documentation.

## Which handler means what

- **onInput**: the value changed while the user is still editing, such as typing. By default it only validates if the field already shows an error.
- **onChange**: the user committed a value, such as selecting an option. It validates by default.
- **onBlur**: focus left the control. It marks the field touched and validates by default.

Pick the handler that matches the interaction. You can change what each one triggers with [`validateOn` and `validationMode`](https://notformdocs.vercel.app/docs/guides/validation#configure-a-field).

<tip>

Keep `v-model` before the event binding. The value has to be written before `onChange` or `onInput` takes its snapshot for validation.

</tip>

## Controls that emit a value

```vue
<template>
  <Field
    v-slot="{ field }"
    :field="form.field('country')"
  >
    <CountryPicker
      v-model="field.model"
      :invalid="!field.isValid"
      @update:model-value="field.events.onChange"
      @focusout="field.events.onBlur"
    />

    <FieldError :errors="field" />
  </Field>
</template>
```

## Controls that forward native events

```vue
<template>
  <Field
    v-slot="{ field }"
    :field="form.field('displayName')"
  >
    <TextInput
      v-model="field.model"
      v-bind="field.events"
    />

    <FieldError :errors="field" />
  </Field>
</template>
```

`field.events` contains `onBlur`, `onChange`, and `onInput`. If the component does not forward those events, bind the callbacks explicitly, as above.

## Controls that do not use v-model

If a control exposes a value prop and a callback instead, write through `field.model` yourself:

```vue
<template>
  <RatingInput
    :value="field.model"
    @rate="(value) => {
      field.model = value
      field.events.onChange()
    }"
  />
</template>
```

Assign first, then call the event.

## A reusable field wrapper

Most apps wrap label, input, and error once. Accept a field instance as a prop.

```vue
<script setup lang="ts">
import { FieldError, type FieldInstance } from 'notform'

defineProps<{
  field: FieldInstance<string>
  label: string
  type?: string
}>()
</script>

<template>
  <div class="field">
    <label :for="field.path">{{ label }}</label>

    <input
      :id="field.path"
      v-model="field.model"
      v-bind="field.events"
      :type="type ?? 'text'"
      :aria-invalid="!field.isValid"
    >

    <FieldError
      :errors="field"
      class="field-error"
      role="alert"
    />
  </div>
</template>
```

```vue
<template>
  <AppTextField
    :field="form.field('email')"
    label="Email"
    type="email"
  />
</template>
```

The parent keeps the form instance and passes only the field. Array items are fields too, so `<AppTextField :field="item.field('email')" />` works inside `ArrayField`.

## Checklist for a new control

- [ ] Does `field.model` reflect the control's value type? Make sure the schema accepts that type (for example `boolean`, `string[]`, or a number).
- [ ] Does the control have an element that can take focus, so `onBlur` can fire?
- [ ] Does it have an accessible name? Use `field.path` for ids.
- [ ] Does it show `field.isValid` and the error message?

## Related

- [Binding inputs](https://notformdocs.vercel.app/docs/guides/binding-inputs)
- [Field](https://notformdocs.vercel.app/docs/components/field)

## Sitemap

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