Nuxt UI v3-alpha has been released!

Try it out
Components

Form

Collect and validate form data.

Usage

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

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

The form component requires two props:

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

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

type Schema = InferType<typeof schema>

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

async function onSubmit (event: FormSubmitEvent<Schema>) {
  // Do something with event.data
  console.log(event.data)
}
</script>

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

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

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>
<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({
  email: undefined,
  password: undefined
})

async function onSubmit (event: FormSubmitEvent<Schema>) {
  // Do something with data
  console.log(event.data)
}
</script>

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

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

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>
<script setup lang="ts">
import Joi from 'joi'
import type { FormSubmitEvent } from '#ui/types'

const schema = Joi.object({
  email: Joi.string().required(),
  password: Joi.string()
    .min(8)
    .required()
})

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

async function onSubmit (event: FormSubmitEvent<any>) {
  // Do something with event.data
  console.log(event.data)
}
</script>

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

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

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>
<script setup lang="ts">
import * as v from 'valibot'
import type { FormSubmitEvent } from '#ui/types'

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

type Schema = v.InferOutput<typeof schema>

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

async function onSubmit (event: FormSubmitEvent<Schema>) {
  // Do something with event.data
  console.log(event.data)
}
</script>

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

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

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>
<script setup lang="ts">
import { object, string, nonempty, type Infer } from 'superstruct'
import type { FormSubmitEvent } from '#ui/types'

const schema = object({
  email: nonempty(string()),
  password: nonempty(string())
})

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

type Schema = Infer<typeof schema>

async function onSubmit (event: FormSubmitEvent<Schema>) {
  console.log(event.data)
}
</script>

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

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

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

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 - Error message for display.
  • path - Path to the form element corresponding to the name attribute.
Note that 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({ path: 'email', message: 'Required' })
  if (!state.password) errors.push({ path: 'password', message: 'Required' })
  return errors
}

async function onSubmit (event: FormSubmitEvent<any>) {
  // Do something with data
  console.log(event.data)
}
</script>

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

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

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

This can also be used to integrate with other validation libraries. Here is an example with Vuelidate:

<script setup lang="ts">
import useVuelidate from '@vuelidate/core'

const props = defineProps({
  rules: { type: Object, required: true },
  model: { type: Object, required: true }
})

const form = ref();
const v = useVuelidate(props.rules, props.model)

async function validateWithVuelidate() {
  v.value.$touch()
  await v.value.$validate()
  return v.value.$errors.map((error) => ({
    message: error.$message,
    path: error.$propertyPath,
  }))
}

defineExpose({
  validate: async () => {
    await form.value.validate()
  }
})
</script>

<template>
  <UForm ref="form" :state="model" :validate="validateWithVuelidate">
    <slot />
  </UForm>
</template>

Backend validation

You can manually set errors after form submission if required. To do this, simply use the form.setErrors function to set the errors as needed.

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

interface Schema {
  email?: string
  password?: string
}

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

const form = ref<Form<Schema>>()

async function onSubmit (event: FormSubmitEvent<Schema>) {
  form.value!.clear()
  try {
    const response = await $fetch('...')
    // ...
  } catch (err) {
    if (err.statusCode === 422) {
      form.value!.setErrors(err.data.errors.map((err) => ({
        // Map validation errors to { path: string, message: string }
        message: err.message,
        path: err.path,
      })))
    }
  }
}
</script>

<template>
  <UForm ref="form" :state="state" @submit="onSubmit">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

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

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

Input events

The Form component automatically triggers validation upon submit, input, blur or change events.

This ensures that any errors are displayed as soon as the user interacts with the form elements. You can control when validation happens this using the validate-on prop.

Note that the input event is not triggered until after the initial blur event. This is to prevent the form from being validated as the user is typing. You can override this behavior by setting the eager-validation prop on FormGroup to true.

Take a look at the component!

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 identifier of the form element.
  • path - the path to the form element matching the name.
  • 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({ path: 'email', message: 'Required' })
  if (!state.password) errors.push({ path: 'password', message: 'Required' })
  return errors
}

async function onSubmit (event: FormSubmitEvent<any>) {
  // Do something with data
  console.log(event.data)
}

async function onError (event: FormErrorEvent) {
  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">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

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

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

Props

staterequired
Record<string, any>
schema
ZodType<any, ZodTypeDef, any> | ObjectSchema<any, AnyObject, any, ""> | AnySchema<any> | BaseSchema<any, any> | ... 7 more ... | Struct<...>
undefined
validate
((state: any) => Promise<FormError<string>[]>) | ((state: any) => FormError<string>[])
[]
validateOn
FormEventType[]
["blur", "input", "change", "submit"]

API

When accessing the component via a template ref, you can use the following:

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)

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)

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

errors
Ref<FormError[]>

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