Forms
Basic pattern
Nuxt UI forms use UForm + UFormField + Standard Schema validation (Zod, Valibot, Yup, or Joi).
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.email('Invalid email'),
password: z.string().min(8, 'Min 8 characters')
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({ email: '', password: '' })
function onSubmit(event: FormSubmitEvent<Schema>) {
// UForm validates before emitting @submit — access validated data via event.data
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField name="email" label="Email" required>
<UInput v-model="state.email" type="email" placeholder="[email protected]" />
</UFormField>
<UFormField name="password" label="Password" required>
<UInput v-model="state.password" type="password" placeholder="Min 8 characters" />
</UFormField>
<UButton type="submit" label="Sign in" />
</UForm>
</template>Key rules
- Always use
UFormFieldaround inputs — it connects validation errors via thenameprop - The
nameprop onUFormFieldmust match the schema field name exactly - Use
reactive<Partial<Schema>>({})for state —Partialallows empty initial values @submitonly fires when validation passes- For nested objects, use dot notation:
name="address.city"
UFormField props
| Prop | Purpose |
|---|---|
name | Links to schema field for validation errors |
label | Visible label text |
description | Help text below the input |
hint | Right-aligned hint text (e.g., "Optional") |
required | Shows required indicator |
size | Inherits to child input |
Field layout patterns
Vertical stack (default)
<UForm :schema="schema" :state="state" class="space-y-4">
<UFormField name="name" label="Name">
<UInput v-model="state.name" />
</UFormField>
<UFormField name="email" label="Email">
<UInput v-model="state.email" />
</UFormField>
</UForm>Inline fields with UFieldGroup
<UFieldGroup>
<UFormField name="firstName" label="First name">
<UInput v-model="state.firstName" />
</UFormField>
<UFormField name="lastName" label="Last name">
<UInput v-model="state.lastName" />
</UFormField>
</UFieldGroup>Grid layout
<UForm :schema="schema" :state="state" class="grid grid-cols-2 gap-4">
<UFormField name="firstName" label="First name">
<UInput v-model="state.firstName" />
</UFormField>
<UFormField name="lastName" label="Last name">
<UInput v-model="state.lastName" />
</UFormField>
<UFormField name="email" label="Email" class="col-span-2">
<UInput v-model="state.email" type="email" />
</UFormField>
</UForm>Common field patterns
Select
<UFormField name="role" label="Role">
<USelect v-model="state.role" :items="['Admin', 'Editor', 'Viewer']" placeholder="Choose role" />
</UFormField>Checkbox
<UFormField name="terms">
<UCheckbox v-model="state.terms" label="I agree to the terms and conditions" />
</UFormField>Radio group
<UFormField name="plan" label="Plan">
<URadioGroup
v-model="state.plan"
:items="[
{ label: 'Free', value: 'free', description: 'For personal projects' },
{ label: 'Pro', value: 'pro', description: 'For teams' }
]"
/>
</UFormField>Switch
<UFormField name="notifications" label="Email notifications">
<USwitch v-model="state.notifications" />
</UFormField>Textarea
<UFormField name="bio" label="Bio" description="Brief description for your profile.">
<UTextarea v-model="state.bio" :rows="3" autoresize :maxrows="6" />
</UFormField>File upload
<UFormField name="avatar" label="Avatar">
<UFileUpload v-model="state.avatar" accept="image/*" />
</UFormField>
<!-- Or as a drop area -->
<UFormField name="documents" label="Documents">
<UFileUpload v-model="state.documents" multiple variant="area" />
</UFormField>Date
<UFormField name="date" label="Date">
<UInputDate v-model="state.date" />
</UFormField>
<!-- Date range -->
<UFormField name="dateRange" label="Date range">
<UInputDate v-model="state.dateRange" range />
</UFormField>Programmatic validation
<script setup lang="ts">
const form = useTemplateRef('form')
async function validateAndSubmit() {
const result = await form.value?.validate()
if (result) {
// valid — submit
}
}
async function validateEmail() {
await form.value?.validate({ name: 'email' })
}
function setServerError() {
form.value?.setErrors([
{ name: 'email', message: 'Email already taken' }
])
}
function resetErrors() {
form.value?.clearErrors()
}
</script>
<template>
<UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
<!-- fields -->
</UForm>
</template>Form in a modal
Use #footer="{ close }" scoped slot for cancel/submit actions. Wrap the modal body in UForm with a type="submit" button in the footer so validation runs on submit.
<UModal v-model:open="isOpen" title="Edit profile" description="Update your information." :ui="{ footer: 'justify-end' }">
<template #body>
<UForm id="profile-form" :schema="schema" :state="state" class="space-y-4" @submit="onSave">
<UFormField name="name" label="Name">
<UInput v-model="state.name" />
</UFormField>
<UFormField name="email" label="Email">
<UInput v-model="state.email" type="email" />
</UFormField>
</UForm>
</template>
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton type="submit" form="profile-form" label="Save" />
</template>
</UModal>