Form
Schema-validated fields, composed from Form and FormField.
Composing a form
A form is two components. Form owns validation and submission: a Zod schema plus a reactive state object, with no field UI of its own. FormField wraps one control with its label, optional description or hint, and validation error. Every field's name must match a key in the schema and state; that is what routes an error to the right FormField.
A basic form
<template> <Form :schema="schema" :state="state" @submit="onSubmit"> <FormField label="Email" name="email"> <Input v-model="state.email" placeholder="you@example.com" class="w-full" /> </FormField> <Button html-type="submit" class="self-start">Submit</Button> </Form></template>Field types
Every control, one schema
<template> <Form :schema="schema" :state="state" @submit="onSubmit"> <FormField label="Name" name="name"> <Input v-model="state.name" placeholder="Jane Doe" class="w-full" /> </FormField> <FormField label="Bio" name="bio"> <Textarea v-model="state.bio" :rows="3" class="w-full" /> </FormField> <FormField label="Role" name="role"> <Select v-model="state.role" :items="roleOptions" class="w-full" /> </FormField> <FormField label="Seats" name="seats"> <InputNumber v-model="state.seats" :min="1" :step="1" class="w-32" /> </FormField> <FormField label="Plan" name="plan"> <RadioGroup v-model="state.plan" :items="planOptions" /> </FormField> <FormField name="notify"> <Switch v-model="state.notify" label="Email notifications" /> </FormField> <Button html-type="submit">Submit</Button> </Form></template>Validating and submitting
- The schema drives field-level errors, routed by
name. - A trigger outside the
<Form>element, such as a drawer or dialog footer, callsformRef.value?.validate()from a template ref. See the drawer example below. - Enter submits only when the form has a submit button (
html-type="submit"). Formhas no submitting state. The submit button'sloadingprop is toggled around the async handler.
Examples
Full page composition
A settings form inside a PageSection and a Card, with the submit button inside the form and its loading prop toggled around the async handler.
Profile
Your name and contact email.
<template> <PageSection title="Profile" description="Your name and contact email."> <Card> <Form :schema="schema" :state="state" class="flex flex-col gap-4" @submit="onSubmit"> <FormField label="Display name" name="displayName"> <Input v-model="state.displayName" class="w-full" /> </FormField> <FormField label="Email" name="email"> <Input v-model="state.email" class="w-full" /> </FormField> <Button html-type="submit" :loading="saving" class="self-start">Save</Button> </Form> </Card> </PageSection></template>Drawer footer triggers the form
The footer stays fixed while the body scrolls, so its Save button sits outside the Form element and calls formRef.value?.validate().
<template> <Drawer v-model:open="open" title="New automation"> <Form ref="formRef" :schema="schema" :state="state" class="flex flex-col gap-4"> <FormField label="Title" name="title"> <Input v-model="state.title" class="w-full" /> </FormField> <FormField label="Description" name="description"> <Textarea v-model="state.description" :rows="3" class="w-full" /> </FormField> <FormField label="Trigger" name="trigger"> <Select v-model="state.trigger" :items="triggerOptions" class="w-full" /> </FormField> <FormField name="enabled"> <Switch v-model="state.enabled" label="Enabled" /> </FormField> </Form> <template #footer> <Button variant="outline" @click="open = false">Cancel</Button> <Button :loading="saving" @click="onSave">Save</Button> </template> </Drawer></template>Props
| Name | Type | Default |
|---|---|---|
| schema | ZodSchema | ... | - |
| state | object | - |
| ...props | UForm props (validate, validateOn, disabled, name, nested, transform, id, ...) | - |
Slots
| Name | Description |
|---|---|
| default | Form fields, scoped with `errors` and `loading`. |
Guidelines
Do
- Give every field a
namethat matches its key inschemaandstate. - Wrap every control in a
FormField, including aSwitchorCheckboxthat carries its own label. - Include a submit button with
html-type="submit"so that Enter submits the form.
- Call
formRef.value?.validate()from a template ref when the trigger sits outside the<Form>element.
Don't
- Don't validate with
watchor per-field checks when the schema already expresses the rule. - Don't track a submitting state in
Form; toggle the submit button'sloadingprop instead. - Don't build a form from bare controls without
Form; validation and error routing only work through it.