Skip to main content
Vuetify0 v1.0 is here
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

Form

A form wrapper that coordinates validation across child input fields and handles submit/reset events.

Edit this page
Report a Bug
Open issues
View on GitHub
Copy Markdown

PreviewRenders elementIntermediateJul 15, 2026

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

vue
<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.

Form Architecture

Use controls to zoom and pan. Click outside or press Escape to close.

Form Architecture

The @submit event is pass-through — it always fires, regardless of validity. Guard in your handler:

ts
function onSubmit ({ valid }: { valid: boolean }) {
  if (!valid) return
  // handle submission
}

Examples

Account Sign-Up

A complete account registration form that shows how Form coordinates validation across several Input.Root fields and reports a single aggregate result. The form gathers name, email, password, and a confirmation, validates each field on blur, and only commits when every child validation passes.

The interesting part is cross-field validation. The confirmation field’s rule compares its value against the live password model rather than a static value, so changing the password re-evaluates whether the two still match. Because Form’s @submit event is pass-through — it fires on every native submit regardless of validity — the handler guards on payload.valid before doing any work, then simulates a server-side duplicate-account check that surfaces through the email field’s error / error-messages props. The composable owns the field state and the submitted/server-error flags, so the entry component can swap the form for a success panel without the form needing to know about it.

Reach for this pattern whenever a form is more than a single field: extract the state and submit logic into a use* composable, keep the markup in a reusable component, and let the entry wire them together. For the underlying validation primitives, see createForm and createValidation; for individual field anatomy, see Input.

FileRole
useSignup.tsComposable — field state, submit/reset logic, simulated server error
SignupForm.vueReusable component — renders the Form with four validated Input.Root fields and a cross-field match rule
signup-form.vueEntry — wires the composable to the form and swaps in a success panel on submit

Try submitting with taken@example.com to see server-side error injection.

Recipes

Slot Props

Use slot props for reactive form state in the template:

vue
<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():

vue
<template>
  <Form disabled>
    <!-- All fields read form.disabled via useForm() -->
  </Form>
</template>

Custom Namespace

Use namespace to isolate multiple forms on the same page:

vue
<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:

vue
<template>
  <Form v-slot="{ submit }">
    <!-- Inputs -->

    <button type="button" @click="submit">Save Draft</button>
  </Form>
</template>
Tip

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.

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

KeyBehavior
Enter (in input)Submits the form
EscapeNo default behavior — handle in your submit handler

FAQ

Discord
Need help? Join our community for support and discussions ↗

API Reference

The following API details are for all variations of the Form component.
Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/