Form

A form component with built-in validation and submission handling.

Usage

Use the Form component to validate form data using schema libraries such as Zod, Yup, Joi, Valibot, Superstruct or your own validation logic.

It works with the FormField component to display error messages around form elements automatically.

Schema Validation

It requires two props:

  • state - a reactive object holding the form's state.
  • schema - a schema object from a validation library like Zod, Yup, Joi, Valibot or Superstruct.
No validation library is included by default, ensure you install the one you need.
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '#ui/types'

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Must be at least 8 characters')
})

type Schema = z.output<typeof schema>

const state = reactive<Partial<Schema>>({
  email: undefined,
  password: undefined
})

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormField label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormField>

    <UFormField label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormField>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Errors are reported directly to the FormField component based on the name or error-pattern prop. This means the validation rules defined for the email attribute in your schema will be applied to <FormField name="email">.

Nested validation rules are handled using dot notation. For example, a rule like { user: z.object({ email: z.string() }) } will be applied to <FormField name="user.email">.

Custom Validation

Use the validate prop to apply your own validation logic.

The validation function must return a list of errors with the following attributes:

  • message - the error message to display.
  • name - the name of the FormField to send the error to.
It can be used alongside the schema prop to handle complex use cases.
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '#ui/types'

const state = reactive({
  email: undefined,
  password: undefined
})

const validate = (state: any): FormError[] => {
  const errors = []
  if (!state.email) errors.push({ name: 'email', message: 'Required' })
  if (!state.password) errors.push({ name: 'password', message: 'Required' })
  return errors
}

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<any>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm :validate="validate" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormField label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormField>

    <UFormField label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormField>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Input Events

The Form component automatically triggers validation when an input emits an input, change, or blur event.

  • Validation on input occurs as you type.
  • Validation on change occurs when you commit to a value.
  • Validation on blur happens when an input loses focus.

You can control when validation happens this using the validate-on prop.

Radio group
You can use the useFormField composable to implement this inside your own components.

Error Event

You can listen to the @error event to handle errors. This event is triggered when the form is submitted and contains an array of FormError objects with the following fields:

  • id - the input's id.
  • name - the name of the FormField
  • message - the error message to display.

Here's an example that focuses the first input element with an error after the form is submitted:

<script setup lang="ts">
import type { FormError, FormErrorEvent, FormSubmitEvent } from '#ui/types'

const state = reactive({
  email: undefined,
  password: undefined
})

const validate = (state: any): FormError[] => {
  const errors = []
  if (!state.email) errors.push({ name: 'email', message: 'Required' })
  if (!state.password) errors.push({ name: 'password', message: 'Required' })
  return errors
}

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<any>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}

async function onError(event: FormErrorEvent) {
  if (event?.errors?.[0]?.id) {
    const element = document.getElementById(event.errors[0].id)
    element?.focus()
    element?.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }
}
</script>

<template>
  <UForm :validate="validate" :state="state" class="space-y-4" @submit="onSubmit" @error="onError">
    <UFormField label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormField>

    <UFormField label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormField>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Nesting Forms

Nesting form components allows you to manage complex data structures, such as lists or conditional fields, more efficiently.

For example, it can be used to dynamically add fields based on user's input:

<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const schema = z.object({
  name: z.string().min(2),
  news: z.boolean()
})

type Schema = z.output<typeof schema>

const nestedSchema = z.object({
  email: z.string().email()
})

type NestedSchema = z.output<typeof nestedSchema>

const state = reactive<Partial<Schema & NestedSchema>>({ })

const toast = useToast()
async function onSubmit(event: FormSubmitEvent<any>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm
    :state="state"
    :schema="schema"
    class="gap-4 flex flex-col w-60"
    @submit="onSubmit"
  >
    <UFormField label="Name" name="name">
      <UInput v-model="state.name" placeholder="John Lennon" />
    </UFormField>

    <div>
      <UCheckbox v-model="state.news" name="news" label="Register to our newsletter" />
    </div>

    <UForm v-if="state.news" :state="state" :schema="nestedSchema">
      <UFormField label="Email" name="email">
        <UInput v-model="state.email" placeholder="[email protected]" />
      </UFormField>
    </UForm>

    <div>
      <UButton type="submit">
        Submit
      </UButton>
    </div>
  </UForm>
</template>

Or to validate list inputs:

<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const schema = z.object({
  customer: z.string().min(2)
})

type Schema = z.output<typeof schema>

const itemSchema = z.object({
  description: z.string().min(1),
  price: z.number().min(0)
})

type ItemSchema = z.output<typeof itemSchema>

const state = reactive<Partial<Schema & { items: Partial<ItemSchema>[] }>>({
  items: [{}]
})

function addItem() {
  if (!state.items) {
    state.items = []
  }
  state.items.push({})
}

function removeItem() {
  if (state.items) {
    state.items.pop()
  }
}

const toast = useToast()

async function onSubmit(event: FormSubmitEvent<any>) {
  toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
  console.log(event.data)
}
</script>

<template>
  <UForm
    :state="state"
    :schema="schema"
    class="gap-4 flex flex-col w-60"
    @submit="onSubmit"
  >
    <UFormField label="Customer" name="customer">
      <UInput v-model="state.customer" placeholder="Wonka Industries" />
    </UFormField>

    <UForm v-for="item, count in state.items" :key="count" :state="item" :schema="itemSchema" class="flex gap-2">
      <UFormField :label="!count ? 'Description' : undefined" name="description">
        <UInput v-model="item.description" />
      </UFormField>
      <UFormField :label="!count ? 'Price' : undefined" name="price" class="w-20">
        <UInput v-model="item.price" type="number" />
      </UFormField>
    </UForm>

    <div class="flex gap-2">
      <UButton color="neutral" variant="subtle" size="sm" @click="addItem()">
        Add Item
      </UButton>

      <UButton color="neutral" variant="ghost" size="sm" @click="removeItem()">
        Remove Item
      </UButton>
    </div>
    <div>
      <UButton type="submit">
        Submit
      </UButton>
    </div>
  </UForm>
</template>

API

Props

Prop Default Type
state

object

id

string | number

schema

ZodType<any, ZodTypeDef, any> | GenericSchema<unknown, unknown, BaseIssue<unknown>> | GenericSchemaAsync<unknown, unknown, BaseIssue<unknown>> | (input: unknown): SafeParseResult<any> | (input: unknown): Promise<SafeParseResult<any>> | Struct<any, any> | StandardSchema<unknown, unknown> | ObjectSchema<object, AnyObject, any, ""> | AnySchema<object> | ArraySchema<object> | AlternativesSchema<object> | BinarySchema<object> | BooleanSchema<object> | DateSchema<object> | FunctionSchema<object> | NumberSchema<object> | ObjectSchema<object> | StringSchema<object> | LinkSchema<object> | SymbolSchema<object>

validate

(state: object): FormError<string>[] | Promise<FormError<string>[]>

validateOn

FormInputEvents[]

disabled

boolean

validateOnInputDelay

300

number

Slots

Slot Type
default

{}

Emits

Event Type
submit

FormSubmitEvent<object>

error

FormErrorEvent

Expose

You can access the typed component instance using useTemplateRef.

<script setup lang="ts">
const form = useTemplateRef('form')
</script>

<template>
  <UForm ref="form" />
</template>

This will give you access to the following:

NameType
submit()Promise<void>

Triggers form submission.

validate(path?: string | string[], opts: { silent?: boolean })Promise<T>

Triggers form validation. Will raise any errors unless opts.silent is set to true.

clear(path?: string)void

Clears form errors associated with a specific path. If no path is provided, clears all form errors.

getErrors(path?: string)FormError[]

Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.

setErrors(errors: FormError[], path?: string)void

Sets form errors for a given path. If no path is provided, overrides all errors.

errorsRef<FormError[]>

A reference to the array containing validation errors. Use this to access or manipulate the error information.

disabledRef<boolean>