---
title: "ShadcnVue"
description: "Use NotForm with ShadcnVue inputs, including text, textarea, select, checkbox, radio group, switch, and array fields, and avoid the Field and FieldError naming conflict."
canonical_url: "https://notformdocs.vercel.app/docs/integrations/shadcn-vue"
---

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

ShadcnVue copies component source into your project, so the exact props depend on the version you installed. The mappings below follow the pattern in [Custom components](https://notformdocs.vercel.app/docs/integrations/custom-components). Check your local component files if a binding differs.

## Resolve the naming conflict

Recent ShadcnVue releases include `Field` and `FieldError` components. NotForm exports components with the same names. Rename one set at the import.

<code-group>

```vue [Vue]
<script setup lang="ts">
import { Form, Field as NotField, FieldError as NotFieldError, useForm } from 'notform'
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
</script>
```

```ts [Nuxt]
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@notform/nuxt', 'shadcn-vue/nuxt'],
  notform: {
    aliases: {
      Field: 'NotField',
      FieldError: 'NotFieldError'
    },
  },
})
```

</code-group>

The examples below use `NotField` for NotForm and `Field` for ShadcnVue.

## Choose the right binding

Every `NotField` slot gives you a `field` with a `model`, a set of `events`, and the validation state (`isValid` and `errors`). How you connect them depends on what the ShadcnVue component emits.

<table>
<thead>
  <tr>
    <th>
      Component
    </th>
    
    <th>
      Binding
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Input
      </code>
      
      , <code>
        Textarea
      </code>
      
      , <code>
        InputGroupInput
      </code>
      
      , <code>
        InputGroupTextarea
      </code>
    </td>
    
    <td>
      <code>
        v-model="field.model"
      </code>
      
       and <code>
        v-bind="field.events"
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Select
      </code>
    </td>
    
    <td>
      <code>
        v-model="field.model"
      </code>
      
       and <code>
        @update:model-value="field.events.onChange"
      </code>
      
      , with <code>
        @blur="field.events.onBlur"
      </code>
      
       on <code>
        SelectTrigger
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Checkbox
      </code>
      
      , <code>
        Switch
      </code>
      
      , <code>
        RadioGroup
      </code>
    </td>
    
    <td>
      <code>
        v-model="field.model"
      </code>
      
      , <code>
        @update:model-value="field.events.onChange"
      </code>
      
       and <code>
        @blur="field.events.onBlur"
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Checkbox group (array of values)
    </td>
    
    <td>
      <code>
        :model-value
      </code>
      
       and a handler that updates <code>
        field.model
      </code>
      
      , then calls <code>
        field.events.onChange()
      </code>
    </td>
  </tr>
</tbody>
</table>

Components that only emit `update:modelValue` don't fire native `input` or `change` events, so `v-bind="field.events"` does nothing for them. Report changes and blur explicitly instead.

Each section below shows the template and the schema it binds to. The snippets assume the `form` and the imports from the [full example](#full-example).

## Text input

```ts
title: z
  .string()
  .min(5, 'Bug title must be at least 5 characters.')
  .max(32, 'Bug title must be at most 32 characters.')
```

<code-collapse>

```vue
<template>
  <NotField
    v-slot="{ field }"
    :field="form.field('title')"
  >
    <Field :data-invalid="!field.isValid">
      <FieldLabel :for="field.path">
        Bug Title
      </FieldLabel>

      <Input
        :id="field.path"
        v-bind="field.events"
        v-model="field.model"
        placeholder="Login button not working on mobile"
        autocomplete="off"
        :aria-invalid="!field.isValid"
      />

      <FieldError
        v-if="!field.isValid"
        :errors="field.errors"
      />
    </Field>
  </NotField>
</template>
```

</code-collapse>

`field.errors` is an array of issues that each have a `message`, which is the shape ShadcnVue's `FieldError` renders.

## Textarea

Use `InputGroupTextarea` inside an `InputGroup` when you want an addon, such as a character counter. It takes the same bindings as a text input.

```ts
description: z
  .string()
  .min(20, 'Description must be at least 20 characters.')
  .max(100, 'Description must be at most 100 characters.')
```

<code-collapse>

```vue
<template>
  <NotField
    v-slot="{ field }"
    :field="form.field('description')"
  >
    <Field :data-invalid="!field.isValid">
      <FieldLabel :for="field.path">
        Description
      </FieldLabel>

      <InputGroup>
        <InputGroupTextarea
          :id="field.path"
          v-model="field.model"
          v-bind="field.events"
          placeholder="I'm having an issue with the login button on mobile."
          :rows="6"
          class="resize-none min-block-24"
          :aria-invalid="!field.isValid"
        />

        <InputGroupAddon align="block-end">
          <InputGroupText class="tabular-nums">
            {{ field.model?.length || 0 }}/100 characters
          </InputGroupText>
        </InputGroupAddon>
      </InputGroup>

      <FieldDescription>
        Include steps to reproduce, expected behavior, and what actually
        happened.
      </FieldDescription>

      <FieldError
        v-if="!field.isValid"
        :errors="field.errors"
      />
    </Field>
  </NotField>
</template>
```

</code-collapse>

`field.model` is reactive, so you can read it anywhere in the slot, which is how the counter stays in sync.

## Select

The trigger is the focusable element, so put the `id` and report blur from it.

```ts
language: z.string().min(1, 'Please select your spoken language.')
```

<code-collapse>

```vue
<template>
  <NotField
    v-slot="{ field }"
    :field="form.field('language')"
  >
    <Field :data-invalid="!field.isValid">
      <FieldLabel :for="field.path">
        Spoken Language
      </FieldLabel>

      <!-- eslint-disable-next-line vue-a11y/form-control-has-label -->
      <Select
        v-model="field.model"
        @update:model-value="field.events.onChange"
      >
        <SelectTrigger
          :id="field.path"
          :aria-invalid="!field.isValid"
          @blur="field.events.onBlur()"
        >
          <SelectValue placeholder="Select" />
        </SelectTrigger>

        <SelectContent>
          <SelectItem
            v-for="language in spokenLanguages"
            :key="language.value"
            :value="language.value"
          >
            {{ language.label }}
          </SelectItem>
        </SelectContent>
      </Select>

      <FieldError
        v-if="!field.isValid"
        :errors="field.errors"
      />
    </Field>
  </NotField>
</template>
```

</code-collapse>

Keep an empty string as the initial value so the placeholder shows until the user picks something.

## Checkbox

Bind the checkbox with `v-model:model-value`, then report changes and blur to NotForm yourself: call `field.events.onChange` from `update:model-value` and `field.events.onBlur` from `blur`.

```ts
responses: z.literal(true, 'Notifications are mandatory')
```

<code-collapse>

```vue
<template>
  <NotField
    v-slot="{ field }"
    :field="form.field('responses')"
  >
    <Field
      orientation="horizontal"
      :data-invalid="!field.isValid"
    >
      <Checkbox
        :id="field.path"
        v-model="field.model"
        :aria-invalid="!field.isValid"
        @blur="field.events.onBlur"
        @update:model-value="field.events.onChange"
      />

      <FieldLabel
        :for="field.path"
        class="font-normal"
      >
        Push notifications
      </FieldLabel>
    </Field>

    <FieldError
      v-if="!field.isValid"
      :errors="field.errors"
    />
  </NotField>
</template>
```

</code-collapse>

If the checkbox can be in an indeterminate state, its value is not a plain boolean, so your schema must accept that.

## Checkbox group

When several checkboxes share one field, store the selected values in an array. The checkbox doesn't bind to `field.model` directly. Instead, derive its checked state and write the new array back yourself.

```ts
tasks: z
  .array(z.string())
  .min(1, 'Please select at least one notification type.')
```

<code-collapse>

```vue
<template>
  <NotField
    v-slot="{ field }"
    :field="form.field('tasks')"
  >
    <FieldSet :data-invalid="!field.isValid">
      <FieldLegend variant="label">
        Tasks
      </FieldLegend>

      <FieldDescription>
        Get notified when tasks you've created have updates.
      </FieldDescription>

      <FieldGroup data-slot="checkbox-group">
        <Field
          v-for="task in tasks"
          :key="task.id"
          orientation="horizontal"
          :data-invalid="!field.isValid"
        >
          <Checkbox
            :id="`${field.path}-${task.id}`"
            :aria-invalid="!field.isValid"
            :model-value="field.model?.includes(task.id)"
            @blur="field.events.onBlur()"
            @update:model-value="(checked: boolean | 'indeterminate') => {
              field.model = checked === true
                ? [...(field.model ?? []), task.id]
                : (field.model ?? []).filter(value => value !== task.id)

              field.events.onChange()
            }"
          />

          <FieldLabel
            :for="`${field.path}-${task.id}`"
            class="font-normal"
          >
            {{ task.label }}
          </FieldLabel>
        </Field>
      </FieldGroup>

      <FieldError
        v-if="!field.isValid"
        :errors="field.errors"
      />
    </FieldSet>
  </NotField>
</template>
```

</code-collapse>

Give each checkbox a unique `id` by appending the item's id to `field.path`, and call `field.events.onChange()` after you update the model so validation runs against the new value.

## Radio group

Bind the group, not the items. Each `RadioGroupItem` only needs its own `value`.

```ts
plan: z.string().min(1, 'You must select a subscription plan to continue.')
```

<code-collapse>

```vue
<template>
  <NotField
    v-slot="{ field }"
    :field="form.field('plan')"
  >
    <FieldSet :data-invalid="!field.isValid">
      <FieldLegend>Plan</FieldLegend>

      <FieldDescription>
        You can upgrade or downgrade your plan at any time.
      </FieldDescription>

      <RadioGroup
        v-model="field.model"
        :aria-invalid="!field.isValid"
        @blur="field.events.onBlur"
        @update:model-value="field.events.onChange"
      >
        <FieldLabel
          v-for="plan in plans"
          :key="plan.id"
          :for="`${field.path}-${plan.id}`"
        >
          <Field
            orientation="horizontal"
            :data-invalid="!field.isValid"
          >
            <FieldContent>
              <FieldTitle>{{ plan.title }}</FieldTitle>

              <FieldDescription>
                {{ plan.description }}
              </FieldDescription>
            </FieldContent>

            <RadioGroupItem
              :id="`${field.path}-${plan.id}`"
              :value="plan.id"
              :aria-invalid="!field.isValid"
            />
          </Field>
        </FieldLabel>
      </RadioGroup>

      <FieldError
        v-if="!field.isValid"
        :errors="field.errors"
      />
    </FieldSet>
  </NotField>
</template>
```

</code-collapse>

## Switch

`Switch` follows the same pattern as `Checkbox`: a boolean model plus explicit change and blur handlers.

```ts
twoFactor: z.boolean().refine((value) => {
  return value === true
}, 'It is highly recommended to enable two-factor authentication.')
```

<code-collapse>

```vue
<template>
  <NotField
    v-slot="{ field }"
    :field="form.field('twoFactor')"
  >
    <Field
      orientation="horizontal"
      :data-invalid="!field.isValid"
    >
      <FieldContent>
        <FieldLabel :for="field.path">
          Multi-factor authentication
        </FieldLabel>

        <FieldDescription>
          Enable multi-factor authentication to secure your account.
        </FieldDescription>

        <FieldError
          v-if="!field.isValid"
          :errors="field.errors"
        />
      </FieldContent>

      <Switch
        :id="field.path"
        v-model="field.model"
        :aria-invalid="!field.isValid"
        @blur="field.events.onBlur"
        @update:model-value="field.events.onChange"
      />
    </Field>
  </NotField>
</template>
```

</code-collapse>

## Array fields

Use `ArrayField` for lists the user can grow, shrink, and reorder. Each item exposes `item.field('key')`, so you can wrap any of the inputs above in a `NotField`.

```ts
emails: z
  .array(z.object({
    address: z.email('Enter a valid email address.'),
  }))
  .min(2, 'Add at least two email addresses.')
  .max(5, 'You can add up to 5 email addresses.')
```

<code-collapse>

```vue
<template>
  <ArrayField
    v-slot="{ arrayField }"
    :array-field="form.arrayField('emails')"
  >
    <FieldSet :data-invalid="!arrayField.isValid">
      <FieldLegend variant="label">
        Email Addresses
      </FieldLegend>

      <FieldDescription>
        Add up to 5 email addresses where we can contact you.
      </FieldDescription>

      <FieldGroup class="gap-4">
        <NotField
          v-for="item in arrayField.items"
          :key="item.id"
          v-slot="{ field }"
          :field="item.field('address')"
        >
          <Field
            orientation="horizontal"
            :data-invalid="!field.isValid"
          >
            <FieldContent>
              <InputGroup>
                <InputGroupInput
                  :id="field.path"
                  v-model="field.model"
                  v-bind="field.events"
                  :aria-invalid="!field.isValid"
                  placeholder="name@example.com"
                  type="email"
                  autocomplete="email"
                />

                <InputGroupAddon
                  v-if="arrayField.items.length > 1"
                  align="inline-end"
                >
                  <InputGroupButton
                    type="button"
                    variant="ghost"
                    size="icon-xs"
                    :disabled="item.index === 0"
                    :aria-label="`Move email ${item.index + 1} up`"
                    @click="arrayField.move(item.index, item.index - 1)"
                  >
                    <ChevronUp />
                  </InputGroupButton>

                  <InputGroupButton
                    type="button"
                    variant="ghost"
                    size="icon-xs"
                    :disabled="item.index === arrayField.items.length - 1"
                    :aria-label="`Move email ${item.index + 1} down`"
                    @click="arrayField.move(item.index, item.index + 1)"
                  >
                    <ChevronDown />
                  </InputGroupButton>

                  <InputGroupButton
                    type="button"
                    variant="ghost"
                    size="icon-xs"
                    :aria-label="`Remove email ${item.index + 1}`"
                    @click="arrayField.remove(item.index)"
                  >
                    <X />
                  </InputGroupButton>
                </InputGroupAddon>
              </InputGroup>

              <FieldError
                v-if="!field.isValid"
                :errors="field.errors"
              />
            </FieldContent>
          </Field>
        </NotField>

        <Button
          type="button"
          variant="outline"
          size="sm"
          :disabled="arrayField.items.length >= 5"
          @click="arrayField.append({ address: '' })"
        >
          Add Email Address
        </Button>
      </FieldGroup>

      <FieldError
        v-if="!arrayField.isValid"
        :errors="arrayField.errors"
      />
    </FieldSet>
  </ArrayField>
</template>
```

</code-collapse>

A few things to note:

- `arrayField.items` gives you each item's `id` and `index`. Use `item.id` as the `:key`, not the index, so inputs keep their state when you reorder or remove items.
- `arrayField.append`, `arrayField.remove` and `arrayField.move` change the list. Set `type="button"` on every button that calls them so they don't submit the form.
- Errors for a single item come from its `field`. Errors for the list as a whole, such as the `min` and `max` rules, come from `arrayField.errors`.
- Mirror the schema limits in the UI, as the **Add** button does with `arrayField.items.length >= 5`, so users can't reach an invalid state by clicking.

## Full example

This form combines every input above. It assumes `vue`, `zod`, `@lucide/vue` and the ShadcnVue components you've added to your project.

<code-collapse>

```vue
<script setup lang="ts">
import { ChevronDown, ChevronUp, X } from '@lucide/vue'
import { ArrayField, Form, Field as NotField, useForm } from 'notform'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
  FieldTitle,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
  InputGroupText,
  InputGroupTextarea,
} from '@/components/ui/input-group'
import {
  RadioGroup,
  RadioGroupItem,
} from '@/components/ui/radio-group'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'

const spokenLanguages = [
  { label: 'English', value: 'en' },
  { label: 'Spanish', value: 'es' },
  { label: 'French', value: 'fr' },
  { label: 'German', value: 'de' },
  { label: 'Italian', value: 'it' },
  { label: 'Chinese', value: 'zh' },
  { label: 'Japanese', value: 'ja' },
] as const

const plans = [
  {
    description: 'For everyday use with basic features.',
    id: 'starter',
    title: 'Starter (100K tokens/month)',
  },
  {
    description: 'For advanced AI usage with more features.',
    id: 'pro',
    title: 'Pro (1M tokens/month)',
  },
  {
    description: 'For large teams and heavy usage.',
    id: 'enterprise',
    title: 'Enterprise (Unlimited tokens)',
  },
] as const

const tasks = [
  {
    id: 'push',
    label: 'Push notifications',
  },
  {
    id: 'email',
    label: 'Email notifications',
  },
] as const

const schema = z.object({
  description: z
    .string()
    .min(20, 'Description must be at least 20 characters.')
    .max(100, 'Description must be at most 100 characters.'),
  emails: z
    .array(z.object({
      address: z.email('Enter a valid email address.'),
    }))
    .min(2, 'Add at least two email addresses.')
    .max(5, 'You can add up to 5 email addresses.'),
  language: z
    .string()
    .min(1, 'Please select your spoken language.')
    .refine((value) => {
      return value !== 'auto'
    }, 'Auto-detection is not allowed. Please select a specific language.'),
  plan: z.string().min(1, 'You must select a subscription plan to continue.'),
  responses: z.literal(true, 'Notifications are mandatory'),
  tasks: z
    .array(z.string())
    .min(1, 'Please select at least one notification type.')
    .refine((value) => {
      return value.every((taskId) => {
        return tasks.some((task) => {
          return task.id === taskId
        })
      })
    }, 'Invalid notification type selected.'),
  title: z
    .string()
    .min(5, 'Bug title must be at least 5 characters.')
    .max(32, 'Bug title must be at most 32 characters.'),
  twoFactor: z.boolean().refine((value) => {
    return value === true
  }, 'It is highly recommended to enable two-factor authentication.'),
})

const form = useForm({
  initialValues: {
    description: '',
    emails: [{ address: '' }, { address: '' }],
    language: '',
    plan: '',
    responses: true,
    tasks: [],
    title: '',
    twoFactor: false,
  },
  onSubmit(data) {
    console.log(data)
  },
  schema,
})
</script>

<template>
  <Form
    :form
    class="
      mx-auto p-8 inline-full

      sm:max-inline-xl
    "
  >
    <FieldGroup>
      <!-- Text input -->
      <NotField
        v-slot="{ field }"
        :field="form.field('title')"
      >
        <Field :data-invalid="!field.isValid">
          <FieldLabel :for="field.path">
            Bug Title
          </FieldLabel>

          <Input
            :id="field.path"
            v-model="field.model"
            v-bind="field.events"
            placeholder="Login button not working on mobile"
            autocomplete="off"
            :aria-invalid="!field.isValid"
          />

          <FieldError
            v-if="!field.isValid"
            :errors="field.errors"
          />
        </Field>
      </NotField>

      <!-- Textarea -->
      <NotField
        v-slot="{ field }"
        :field="form.field('description')"
      >
        <Field :data-invalid="!field.isValid">
          <FieldLabel :for="field.path">
            Description
          </FieldLabel>

          <InputGroup>
            <InputGroupTextarea
              :id="field.path"
              v-model="field.model"
              v-bind="field.events"
              placeholder="I'm having an issue with the login button on mobile."
              :rows="6"
              class="resize-none min-block-24"
              :aria-invalid="!field.isValid"
            />

            <InputGroupAddon align="block-end">
              <InputGroupText class="tabular-nums">
                {{ field.model?.length || 0 }}/100 characters
              </InputGroupText>
            </InputGroupAddon>
          </InputGroup>

          <FieldDescription>
            Include steps to reproduce, expected behavior, and what actually
            happened.
          </FieldDescription>

          <FieldError
            v-if="!field.isValid"
            :errors="field.errors"
          />
        </Field>
      </NotField>

      <!-- Select -->
      <NotField
        v-slot="{ field }"
        :field="form.field('language')"
      >
        <Field :data-invalid="!field.isValid">
          <FieldContent>
            <FieldLabel :for="field.path">
              Spoken Language
            </FieldLabel>

            <FieldDescription>
              For best results, select the language you speak.
            </FieldDescription>

            <FieldError
              v-if="!field.isValid"
              :errors="field.errors"
            />
          </FieldContent>

          <!-- eslint-disable-next-line vue-a11y/form-control-has-label -->
          <Select
            v-model="field.model"
            @update:model-value="field.events.onChange"
          >
            <SelectTrigger
              :id="field.path"
              :aria-invalid="!field.isValid"
              class="min-inline-[120px]"
              @blur="field.events.onBlur()"
            >
              <SelectValue placeholder="Select" />
            </SelectTrigger>

            <SelectContent position="item-aligned">
              <SelectItem value="auto">
                Auto
              </SelectItem>

              <SelectSeparator />

              <SelectItem
                v-for="language in spokenLanguages"
                :key="language.value"
                :value="language.value"
              >
                {{ language.label }}
              </SelectItem>
            </SelectContent>
          </Select>
        </Field>
      </NotField>

      <!-- Checkbox -->
      <NotField
        v-slot="{ field }"
        :field="form.field('responses')"
      >
        <FieldSet :data-invalid="!field.isValid">
          <FieldLegend variant="label">
            Responses
          </FieldLegend>

          <FieldDescription>
            Get notified for requests that take time, like research or image
            generation.
          </FieldDescription>

          <FieldGroup data-slot="checkbox-group">
            <Field orientation="horizontal">
              <Checkbox
                :id="field.path"
                v-model="field.model"
                @blur="field.events.onBlur"
                @update:model-value="field.events.onChange"
              />

              <FieldLabel
                :for="field.path"
                class="font-normal"
              >
                Push notifications
              </FieldLabel>
            </Field>
          </FieldGroup>

          <FieldError
            v-if="!field.isValid"
            :errors="field.errors"
          />
        </FieldSet>
      </NotField>

      <!-- Checkbox group -->
      <NotField
        v-slot="{ field }"
        :field="form.field('tasks')"
      >
        <FieldSet :data-invalid="!field.isValid">
          <FieldLegend variant="label">
            Tasks
          </FieldLegend>

          <FieldDescription>
            Get notified when tasks you've created have updates.
          </FieldDescription>

          <FieldGroup data-slot="checkbox-group">
            <Field
              v-for="task in tasks"
              :key="task.id"
              orientation="horizontal"
              :data-invalid="!field.isValid"
            >
              <Checkbox
                :id="`${field.path}-${task.id}`"
                :aria-invalid="!field.isValid"
                :model-value="field.model?.includes(task.id)"
                @blur="field.events.onBlur()"
                @update:model-value="(checked: boolean | 'indeterminate') => {
                  field.model = checked === true
                    ? [...(field.model ?? []), task.id]
                    : (field.model ?? []).filter(value => value !== task.id)

                  field.events.onChange()
                }"
              />

              <FieldLabel
                :for="`${field.path}-${task.id}`"
                class="font-normal"
              >
                {{ task.label }}
              </FieldLabel>
            </Field>
          </FieldGroup>

          <FieldError
            v-if="!field.isValid"
            :errors="field.errors"
          />
        </FieldSet>
      </NotField>

      <!-- Radio group -->
      <NotField
        v-slot="{ field }"
        :field="form.field('plan')"
      >
        <FieldSet :data-invalid="!field.isValid">
          <FieldLegend>Plan</FieldLegend>

          <FieldDescription>
            You can upgrade or downgrade your plan at any time.
          </FieldDescription>

          <RadioGroup
            v-model="field.model"
            :aria-invalid="!field.isValid"
            @blur="field.events.onBlur"
            @update:model-value="field.events.onChange"
          >
            <FieldLabel
              v-for="plan in plans"
              :key="plan.id"
              :for="`${field.path}-${plan.id}`"
            >
              <Field
                orientation="horizontal"
                :data-invalid="!field.isValid"
              >
                <FieldContent>
                  <FieldTitle>{{ plan.title }}</FieldTitle>

                  <FieldDescription>
                    {{ plan.description }}
                  </FieldDescription>
                </FieldContent>

                <RadioGroupItem
                  :id="`${field.path}-${plan.id}`"
                  :value="plan.id"
                  :aria-invalid="!field.isValid"
                />
              </Field>
            </FieldLabel>
          </RadioGroup>

          <FieldError
            v-if="!field.isValid"
            :errors="field.errors"
          />
        </FieldSet>
      </NotField>

      <!-- Switch -->
      <NotField
        v-slot="{ field }"
        :field="form.field('twoFactor')"
      >
        <Field
          :data-invalid="!field.isValid"
          orientation="horizontal"
        >
          <FieldContent>
            <FieldLabel :for="field.path">
              Multi-factor authentication
            </FieldLabel>

            <FieldDescription>
              Enable multi-factor authentication to secure your account.
            </FieldDescription>

            <FieldError
              v-if="!field.isValid"
              :errors="field.errors"
            />
          </FieldContent>

          <Switch
            :id="field.path"
            v-model="field.model"
            :aria-invalid="!field.isValid"
            @blur="field.events.onBlur"
            @update:model-value="field.events.onChange"
          />
        </Field>
      </NotField>

      <!-- Array field -->
      <ArrayField
        v-slot="{ arrayField }"
        :array-field="form.arrayField('emails')"
      >
        <FieldSet :data-invalid="!arrayField.isValid">
          <FieldLegend variant="label">
            Email Addresses
          </FieldLegend>

          <FieldDescription>
            Add up to 5 email addresses where we can contact you.
          </FieldDescription>

          <FieldGroup class="gap-4">
            <NotField
              v-for="item in arrayField.items"
              :key="item.id"
              v-slot="{ field }"
              :field="item.field('address')"
            >
              <Field
                orientation="horizontal"
                :data-invalid="!field.isValid"
              >
                <FieldContent>
                  <InputGroup>
                    <InputGroupInput
                      :id="field.path"
                      v-model="field.model"
                      v-bind="field.events"
                      :aria-invalid="!field.isValid"
                      placeholder="name@example.com"
                      type="email"
                      autocomplete="email"
                    />

                    <InputGroupAddon
                      v-if="arrayField.items.length > 1"
                      align="inline-end"
                    >
                      <InputGroupButton
                        type="button"
                        variant="ghost"
                        size="icon-xs"
                        :disabled="item.index === 0"
                        :aria-label="`Move email ${item.index + 1} up`"
                        @click="arrayField.move(item.index, item.index - 1)"
                      >
                        <ChevronUp />
                      </InputGroupButton>

                      <InputGroupButton
                        type="button"
                        variant="ghost"
                        size="icon-xs"
                        :disabled="item.index === arrayField.items.length - 1"
                        :aria-label="`Move email ${item.index + 1} down`"
                        @click="arrayField.move(item.index, item.index + 1)"
                      >
                        <ChevronDown />
                      </InputGroupButton>

                      <InputGroupButton
                        type="button"
                        variant="ghost"
                        size="icon-xs"
                        :aria-label="`Remove email ${item.index + 1}`"
                        @click="arrayField.remove(item.index)"
                      >
                        <X />
                      </InputGroupButton>
                    </InputGroupAddon>
                  </InputGroup>

                  <FieldError
                    v-if="!field.isValid"
                    :errors="field.errors"
                  />
                </FieldContent>
              </Field>
            </NotField>

            <Button
              type="button"
              variant="outline"
              size="sm"
              :disabled="arrayField.items.length >= 5"
              @click="arrayField.append({ address: '' })"
            >
              Add Email Address
            </Button>
          </FieldGroup>

          <FieldError
            v-if="!arrayField.isValid"
            :errors="arrayField.errors"
          />
        </FieldSet>
      </ArrayField>

      <Field
        orientation="horizontal"
        class="*:flex-1"
      >
        <Button
          type="reset"
          variant="outline"
        >
          Reset
        </Button>

        <Button type="submit">
          Submit
        </Button>
      </Field>
    </FieldGroup>
  </Form>
</template>
```

</code-collapse>

## Related

- [Custom components](https://notformdocs.vercel.app/docs/integrations/custom-components)
- [Nuxt module aliases](https://notformdocs.vercel.app/docs/getting-started/nuxt-module#avoid-name-conflicts)

## Sitemap

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