Form
A headless form component that coordinates validation across child fields. Renders a native <form> element and intercepts submit/reset — child inputs using createValidation (including Input.Root) auto-register via useForm() injection.
Usage
Wrap your inputs in <Form>. Native <button type="submit"> and <button type="reset"> work as expected.
<script setup lang="ts">
import { Form, Input } from '@vuetify/v0'
import { shallowRef } from 'vue'
const valid = shallowRef<boolean | null>(null)
const email = shallowRef('')
const password = shallowRef('')
const submitted = shallowRef(false)
function onSubmit ({ valid }: { valid: boolean }) {
if (!valid) return
submitted.value = true
}
function onReset () {
email.value = ''
password.value = ''
submitted.value = false
}
</script>
<template>
<Form v-model="valid" class="flex flex-col gap-4 max-w-sm mx-auto" @reset="onReset" @submit="onSubmit">
<Input.Root
id="email"
v-model="email"
label="Email"
:rules="[
(v: string) => !!v || 'Email is required',
(v: string) => /.+@.+\..+/.test(v) || 'Must be a valid email',
]"
type="email"
>
<label class="text-sm font-medium text-on-surface" for="email">Email</label>
<Input.Control
class="w-full px-3 py-2 rounded-lg border border-divider bg-surface text-on-surface placeholder:text-on-surface-variant/50 outline-none data-[focused]:border-primary data-[state=invalid]:border-error transition-colors"
placeholder="you@example.com"
/>
<Input.Error v-slot="{ errors }" class="text-xs text-error">
<span v-for="error in errors" :key="error">{{ error }}</span>
</Input.Error>
</Input.Root>
<Input.Root
id="password"
v-model="password"
label="Password"
:rules="[
(v: string) => !!v || 'Password is required',
(v: string) => v.length >= 8 || 'Must be at least 8 characters',
]"
type="password"
>
<label class="text-sm font-medium text-on-surface" for="password">Password</label>
<Input.Control
class="w-full px-3 py-2 rounded-lg border border-divider bg-surface text-on-surface placeholder:text-on-surface-variant/50 outline-none data-[focused]:border-primary data-[state=invalid]:border-error transition-colors"
placeholder="••••••••"
/>
<Input.Error v-slot="{ errors }" class="text-xs text-error">
<span v-for="error in errors" :key="error">{{ error }}</span>
</Input.Error>
</Input.Root>
<div class="flex gap-2">
<button
class="px-4 py-2 rounded-lg bg-primary text-on-primary font-medium text-sm"
type="submit"
>
Sign in
</button>
<button
class="px-4 py-2 rounded-lg border border-divider text-on-surface text-sm"
type="reset"
>
Reset
</button>
</div>
<p v-if="submitted" class="text-sm text-success">
Form submitted successfully!
</p>
</Form>
</template>
Anatomy
<script setup lang="ts">
import { Form, Input } from '@vuetify/v0'
</script>
<template>
<Form>
<Input.Root>
<Input.Control />
<Input.Error />
</Input.Root>
</Form>
</template>Architecture
Form creates a createForm instance and provides it via useForm(). Child validations auto-register on mount and unregister on unmount.
The @submit event is pass-through — it always fires, regardless of validity. Guard in your handler:
function onSubmit ({ valid }: { valid: boolean }) {
if (!valid) return
// handle submission
}Accessibility
Form renders a native <form> element, so all standard form semantics apply. No custom ARIA is needed — the browser handles submit on Enter, associates labels with inputs via id/for, and reports validation errors to assistive technology through child inputs.
Keyboard Interaction
| Key | Behavior |
|---|---|
Enter (in input) | Submits the form |
Escape | No default behavior — handle in your submit handler |
Form
Props
Events
Slots
default
FormSlotPropsRecipes
Slot Props
Use slot props for reactive form state in the template:
<template>
<Form v-slot="{ isValid, isValidating, submit, reset }">
<!-- Inputs -->
<button type="submit" :disabled="isValidating">
{{ isValidating ? 'Validating…' : 'Submit' }}
</button>
<p v-if="isValid === false">Please fix the errors above.</p>
</Form>
</template>Disabled / Readonly
The disabled and readonly props propagate to the form context. Child components can read them via useForm():
<template>
<Form disabled>
<!-- All fields read form.disabled via useForm() -->
</Form>
</template>Custom Namespace
Use namespace to isolate multiple forms on the same page:
<template>
<Form namespace="billing">
<!-- useForm('billing') resolves this form -->
</Form>
<Form namespace="shipping">
<!-- useForm('shipping') resolves this form -->
</Form>
</template>Programmatic Submit
Call submit() from slot props when you need to trigger validation without a submit button:
<template>
<Form v-slot="{ submit }">
<!-- Inputs -->
<button type="button" @click="submit">Save Draft</button>
</Form>
</template> Calling submit() or reset() via slot props invokes the form methods directly and does not emit @submit or @reset. Those events only fire from native form submission/reset.