Skip to content

Form ​

Form wrapper with layout, validation, and state management for form controls.

Optional short bio
vue
<script setup>
const formRef = ref()
const form = reactive({ name: '', email: '', bio: '' })

const rules = {
  name: [
    { required: true, message: 'Please enter your name' },
    { min: 2, max: 30, message: 'Name must be 2-30 characters' },
  ],
  email: [
    { required: true, message: 'Please enter your email' },
    { type: 'email', message: 'Please enter a valid email' },
  ],
}

async function onSubmit() {
  const { valid } = await formRef.value.validate()
  if (valid) { /* submit */ }
}
</script>

<template>
  <VxForm ref="formRef" :model="form" :rules="rules">
    <VxFormItem label="Name" field="name">
      <VxInput v-model="form.name" />
    </VxFormItem>
    <VxFormItem label="Email" field="email">
      <VxInput v-model="form.email" />
    </VxFormItem>
    <VxFormItem>
      <VxButton @click="onSubmit">Submit</VxButton>
    </VxFormItem>
  </VxForm>
</template>

Horizontal Layout ​

vue
<template>
  <VxForm :model="form" :rules="rules" layout="horizontal" :label-width="120">
    <VxFormItem label="Username" field="username">
      <VxInput v-model="form.username" />
    </VxFormItem>
    <VxFormItem label="Password" field="password">
      <VxInput v-model="form.password" type="password" />
    </VxFormItem>
    <VxFormItem label=" ">
      <VxButton>Login</VxButton>
    </VxFormItem>
  </VxForm>
</template>

Inline Layout ​

All
vue
<template>
  <VxForm :model="form" layout="inline">
    <VxFormItem label="Search">
      <VxInput v-model="form.search" />
    </VxFormItem>
    <VxFormItem label="Category">
      <VxSelect v-model="form.category" :options="options" />
    </VxFormItem>
    <VxFormItem>
      <VxButton>Search</VxButton>
    </VxFormItem>
  </VxForm>
</template>

Label Description ​

Add a tooltip info icon next to the label with description prop or #description slot for rich content.

vue
<template>
  <!-- Simple text description -->
  <VxFormItem label="API Key" description="Your unique API key from the dashboard.">
    <VxInput v-model="form.apiKey" />
  </VxFormItem>

  <!-- Rich content via #description slot -->
  <VxFormItem label="Secret">
    <template #description>
      <div>
        <strong>Signing Secret</strong>
        <p>Used to verify webhook payloads.</p>
      </div>
    </template>
    <VxInput v-model="form.secret" type="password" />
  </VxFormItem>
</template>

Type Validators ​

Built-in type validators: email, url, number, integer, float, date, hex, ip, phone, alpha, alphanumeric, string, boolean, array.

vue
const rules = {
  email: [
    { required: true, message: 'Required' },
    { type: 'email', message: 'Invalid email format' },
  ],
  url: [{ type: 'url', message: 'Invalid URL' }],
  age: [
    { type: 'integer', message: 'Must be a whole number' },
    { min: 1, max: 150, message: 'Age must be 1-150' },
  ],
  ip: [{ type: 'ip', message: 'Invalid IP address' }],
  hex: [{ type: 'hex', message: 'Invalid hex color' }],
  phone: [{ type: 'phone', message: 'Invalid phone number' }],
}

String Constraints ​

Use pattern, enum, startsWith, endsWith, contains, and alpha/alphanumeric type for string validation.

Format: XX-000 or XXX-0000
Letters and numbers only
Must start with #
Must end with .com
admin, editor, or viewer
vue
const rules = {
  code: [
    { required: true, message: 'Required' },
    { pattern: /^[A-Z]{2,3}-\d{3,4}$/, message: 'Format: XX-000 or XXX-0000' },
  ],
  slug: [
    { type: 'alphanumeric', message: 'Only letters and numbers' },
  ],
  tag: [
    { startsWith: '#', message: 'Must start with #' },
    { min: 2, message: 'At least 2 characters' },
  ],
  domain: [
    { endsWith: '.com', message: 'Must end with .com' },
  ],
  role: [
    { enum: ['admin', 'editor', 'viewer'], message: 'Must be admin, editor, or viewer' },
  ],
}

Cross-Field Validation ​

Use same and different to compare fields, and contains for substring checks.

vue
const rules = {
  password: [
    { required: true, message: 'Password is required' },
    { min: 8, message: 'At least 8 characters' },
    { contains: '!', message: 'Must contain !' },
  ],
  confirm: [
    { required: true, message: 'Please confirm' },
    { same: 'password', message: 'Passwords do not match' },
  ],
  newEmail: [
    { required: true, message: 'New email is required' },
    { type: 'email', message: 'Invalid email' },
    { different: 'currentEmail', message: 'Must differ from current email' },
  ],
}

Async Validator ​

Validators can return a Promise. Useful for server-side checks like username availability.

Try 'admin', 'root', or 'test'
vue
const rules = {
  username: [
    { required: true, message: 'Username is required' },
    { type: 'alpha', message: 'Letters only' },
    { min: 3, max: 20, message: '3-20 characters' },
    {
      validator: async (value) => {
        const taken = await checkUsername(value)
        if (taken) return 'Username is already taken'
        return true
      },
      trigger: 'blur',
    },
  ],
}

Reset & Clear Validation ​

vue
<script setup>
const formRef = ref()
</script>

<template>
  <VxForm ref="formRef" :model="form" :rules="rules">
    ...
    <VxButton @click="formRef.validate()">Validate</VxButton>
    <VxButton @click="formRef.clearValidation()">Clear Errors</VxButton>
    <VxButton @click="formRef.resetFields()">Reset</VxButton>
  </VxForm>
</template>

Disabled ​

vue
<template>
  <VxForm :model="form" disabled>
    <VxFormItem label="Name"><VxInput v-model="form.name" /></VxFormItem>
    <VxFormItem label="Email"><VxInput v-model="form.email" /></VxFormItem>
  </VxForm>
</template>

Horizontal Example ​

Select role
vue
<script setup>
const formRef = ref()
const form = reactive({
  firstName: '', lastName: '', email: '',
  role: '', newsletter: false, notes: '',
})

const rules = {
  firstName: { required: true, message: 'First name is required' },
  lastName: { required: true, message: 'Last name is required' },
  email: [
    { required: true, message: 'Email is required' },
    { type: 'email', message: 'Invalid email format' },
  ],
  role: { required: true, message: 'Please select a role' },
}
</script>

<template>
  <VxForm ref="formRef" :model="form" :rules="rules" layout="horizontal" :label-width="120">
    <VxFormItem label="First Name" field="firstName">
      <VxInput v-model="form.firstName" />
    </VxFormItem>
    <VxFormItem label="Role" field="role">
      <VxSelect v-model="form.role" :options="roles" />
    </VxFormItem>
    <VxFormItem label="Newsletter">
      <VxSwitch v-model="form.newsletter" />
    </VxFormItem>
    <VxFormItem label=" ">
      <VxButton @click="formRef.validate()">Submit</VxButton>
    </VxFormItem>
  </VxForm>
</template>

Registration Form ​

Full example with multiple validation rules per field, strong password check, cross-field comparison, and mixed input types.

Optional
HTTPS required
Select role
Max 200 characters
vue
<script setup>
const formRef = ref()
const form = reactive({
  name: '', email: '', password: '', confirmPassword: '',
  phone: '', website: '', role: '', bio: '', terms: false,
})

const rules = {
  name: [
    { required: true, message: 'Full name is required' },
    { min: 2, max: 50, message: '2-50 characters' },
    { type: 'alpha', message: 'Letters only' },
  ],
  email: [
    { required: true, message: 'Email is required' },
    { type: 'email', message: 'Enter a valid email address' },
  ],
  password: [
    { required: true, message: 'Password is required' },
    { min: 8, message: 'At least 8 characters' },
    {
      validator: (v) => {
        if (!/[A-Z]/.test(v)) return 'Must contain an uppercase letter'
        if (!/[0-9]/.test(v)) return 'Must contain a number'
        if (!/[^a-zA-Z0-9]/.test(v)) return 'Must contain a special character'
        return true
      },
    },
  ],
  confirmPassword: [
    { required: true, message: 'Confirm your password' },
    { same: 'password', message: 'Passwords do not match' },
  ],
  phone: [{ type: 'phone', message: 'Enter a valid phone number' }],
  website: [
    { type: 'url', message: 'Enter a valid URL' },
    { startsWith: 'https://', message: 'Must use HTTPS' },
  ],
  role: [
    { required: true, message: 'Select a role' },
  ],
  bio: [{ max: 200, message: 'Max 200 characters' }],
  terms: [
    { validator: (v) => v === true ? true : 'You must accept the terms' },
  ],
}
</script>

<template>
  <VxForm ref="formRef" :model="form" :rules="rules">
    <VxFormItem label="Full Name" field="name">
      <VxInput v-model="form.name" />
    </VxFormItem>
    <VxFormItem label="Email" field="email">
      <VxInput v-model="form.email" />
    </VxFormItem>
    <VxFormItem label="Password" field="password">
      <VxInput v-model="form.password" type="password" />
    </VxFormItem>
    <VxFormItem label="Confirm Password" field="confirmPassword">
      <VxInput v-model="form.confirmPassword" type="password" />
    </VxFormItem>
    <VxFormItem label="Phone" field="phone" help="Optional">
      <VxInput v-model="form.phone" />
    </VxFormItem>
    <VxFormItem label="Website" field="website" help="HTTPS required">
      <VxInput v-model="form.website" />
    </VxFormItem>
    <VxFormItem label="Role" field="role">
      <VxSelect v-model="form.role" :options="roles" />
    </VxFormItem>
    <VxFormItem label="Bio" field="bio" help="Max 200 characters">
      <VxInput v-model="form.bio" type="textarea" :rows="3" />
    </VxFormItem>
    <VxFormItem field="terms">
      <VxCheckbox v-model="form.terms" label="I accept the terms and conditions" />
    </VxFormItem>
    <VxFormItem>
      <VxButton @click="formRef.validate()">Register</VxButton>
    </VxFormItem>
  </VxForm>
</template>

Nested Model ​

Use dot-notation in field to bind and validate nested object properties like user.username.

Optional, max 200 characters
vue
<script setup>
import { ref, reactive } from 'vue'

const formRef = ref()
const form = reactive({
  user: { username: '', password: '' },
  description: '',
})

// Use dot-notation keys to match nested field paths
const rules = {
  'user.username': [
    { required: true, message: 'Username is required' },
    { min: 3, max: 20, message: '3-20 characters' },
    { type: 'alphanumeric', message: 'Letters and numbers only' },
  ],
  'user.password': [
    { required: true, message: 'Password is required' },
    { min: 8, message: 'At least 8 characters' },
    { pattern: /[A-Z]/, message: 'Needs an uppercase letter' },
    { pattern: /[0-9]/, message: 'Needs a number' },
  ],
  description: [
    { max: 200, message: 'Max 200 characters' },
  ],
}

async function onSubmit() {
  const { valid } = await formRef.value.validate()
  if (valid) {
    console.log('Submitted:', JSON.parse(JSON.stringify(form)))
  }
}
</script>

<template>
  <VxForm ref="formRef" :model="form" :rules="rules">
    <VxFormItem label="Username" field="user.username">
      <VxInput v-model="form.user.username" placeholder="Letters and numbers, 3-20 chars" />
    </VxFormItem>
    <VxFormItem label="Password" field="user.password">
      <VxInput v-model="form.user.password" type="password" placeholder="8+ chars, uppercase, number" />
    </VxFormItem>
    <VxFormItem label="Description" field="description" help="Optional, max 200 characters">
      <VxInput v-model="form.description" type="textarea" :rows="3" />
    </VxFormItem>
    <VxFormItem>
      <VxButton @click="onSubmit">Submit</VxButton>
      <VxButton variant="outline" @click="formRef.resetFields()">Reset</VxButton>
    </VxFormItem>
  </VxForm>
</template>

Status Icons ​

Show validation status icons (spinner, checkmark, cross) next to the input.

vue
<template>
  <VxForm ref="formRef" :model="form" :rules="rules" :status-icon="true">
    <VxFormItem label="Email" field="email">
      <VxInput v-model="form.email" />
    </VxFormItem>
  </VxForm>
</template>

Label Suffix ​

Append a suffix character (e.g. :) to all labels.

vue
<template>
  <VxForm :model="form" label-suffix=":">
    <VxFormItem label="Name"><VxInput v-model="form.name" /></VxFormItem>
    <VxFormItem label="Age"><VxInput v-model="form.age" /></VxFormItem>
  </VxForm>
</template>

Scroll to Error ​

Automatically scroll to the first field with an error after validation.

vue
<template>
  <VxForm ref="formRef" :model="form" :rules="rules" :scroll-to-error="true">
    <!-- fields -->
    <VxButton @click="formRef.validate()">Validate All</VxButton>
  </VxForm>
</template>

Show All Errors ​

Display all rule violations at once instead of stopping at the first error.

vue
<template>
  <VxForm :model="form" :rules="rules" :show-all-errors="true">
    <VxFormItem label="Password" field="password">
      <VxInput v-model="form.password" />
    </VxFormItem>
  </VxForm>
</template>

<script setup>
const rules = {
  password: [
    { required: true, message: 'Password is required' },
    { min: 8, message: 'At least 8 characters' },
    { pattern: /[A-Z]/, message: 'Must contain an uppercase letter' },
    { pattern: /[0-9]/, message: 'Must contain a number' },
    { pattern: /[^a-zA-Z0-9]/, message: 'Must contain a special character' },
  ],
}
</script>

Loading State ​

Set loading to disable all interactions and show a loading state. Combines with disabled.

vue
<script setup>
const loading = ref(false)
async function onSubmit() {
  loading.value = true
  await submitForm()
  loading.value = false
}
</script>

<template>
  <VxForm :model="form" :loading="loading">
    <!-- fields -->
    <VxButton @click="onSubmit">Submit</VxButton>
  </VxForm>
</template>

Dirty Tracking ​

Track whether the form has been modified. Access isDirty and dirtyFields via ref.

No changes
vue
<script setup>
const formRef = ref()
const form = reactive({ name: 'John', email: 'john@test.com' })
</script>

<template>
  <VxForm ref="formRef" :model="form">
    <!-- fields -->
    <VxButton :disabled="!formRef?.isDirty">Save Changes</VxButton>
    <span>{{ formRef?.isDirty ? `Modified: ${formRef?.dirtyFields}` : 'No changes' }}</span>
  </VxForm>
</template>

Dependent Field Re-validation ​

Use dependencies prop on FormItem to re-validate when another field changes. Useful for "confirm password" scenarios.

vue
<template>
  <VxForm :model="form" :rules="rules">
    <VxFormItem label="Password" field="password">
      <VxInput v-model="form.password" type="password" />
    </VxFormItem>
    <VxFormItem label="Confirm" field="confirm" :dependencies="['password']">
      <VxInput v-model="form.confirm" type="password" />
    </VxFormItem>
  </VxForm>
</template>

<script setup>
const rules = {
  password: [{ required: true }, { min: 6, message: 'At least 6 characters' }],
  confirm: [{ required: true }, { same: 'password', message: 'Passwords do not match' }],
}
</script>

Required Mark Position ​

Control whether the required asterisk appears on the left or right of the label.

Left (default)
Right
vue
<template>
  <!-- Asterisk on left (default) -->
  <VxForm :model="form" :rules="rules" required-mark-position="left">...</VxForm>

  <!-- Asterisk on right -->
  <VxForm :model="form" :rules="rules" required-mark-position="right">...</VxForm>
</template>

Extra & Error Slots ​

Use #extra for hints below the input, and #error for custom error rendering.

Must be 8+ chars with uppercase, number, and special character.
vue
<template>
  <VxFormItem label="Password" field="password">
    <VxInput v-model="form.password" type="password" />
    <template #extra>
      <span>Must be 8+ chars with uppercase, number, and special character.</span>
    </template>
    <template #error="{ errors, first }">
      <ul>
        <li v-for="e in errors" :key="e">{{ e }}</li>
      </ul>
    </template>
  </VxFormItem>
</template>

Props ​

Form ​

PropTypeDefaultDescription
modelRecord<string, any>—Form data object
rulesFormRules—Validation rules keyed by field name
layout'vertical' | 'horizontal' | 'inline''vertical'Form layout
labelWidthstring | number—Label width for horizontal layout
labelAlign'left' | 'right' | 'top''right'Label text alignment in horizontal layout
labelSuffixstring''Suffix appended to all labels (e.g. :)
size'sm' | 'default' | 'lg''default'Form size
disabledbooleanfalseDisable entire form
loadingbooleanfalseLoading state — disables pointer events
showRequiredMarkbooleantrueShow red asterisk on required fields
requiredMarkPosition'left' | 'right''left'Position of the required asterisk
validateOnChangebooleantrueAuto-validate on model change
validateOnBlurbooleantrueAuto-validate on field blur
hideErrorMessagebooleanfalseHide all error messages
scrollToErrorbooleanfalseScroll to first error after validation
statusIconbooleanfalseShow validation status icons next to inputs
showAllErrorsbooleanfalseShow all validation errors instead of first only

FormItem ​

PropTypeDefaultDescription
labelstring''Label text
fieldstring''Field name (must match key in model and rules)
rulesFormRule | FormRule[]—Field-level rules (merged with form-level)
requiredboolean—Override required state (auto-detected from rules)
labelWidthstring | number—Override form-level label width
showMessagebooleantrueShow error message for this field
helpstring''Help text below the field
descriptionstring''Tooltip text shown via info icon next to label
dependenciesstring[]—Re-validate this field when listed fields change

Events ​

EventPayloadDescription
validate(field, valid, message)Fired after a field is validated
submitEventNative form submit (prevented by default)

Exposed Methods ​

MethodSignatureDescription
validate() => Promise<{ valid, errors }>Validate all fields
validateFields(fields: string[]) => Promise<{ valid, errors }>Validate specific fields
resetFields() => voidReset validation state
clearValidation(fields?: string[]) => voidClear error messages
isDirtyComputedRef<boolean>Whether any field has been modified
dirtyFieldsComputedRef<string[]>List of modified field names
scrollToFirstError(field: string) => voidScroll to a specific field

Slots ​

FormItem ​

SlotPropsDescription
default—Form control
label—Custom label content
label-suffix—Content after the label text
description—Rich tooltip content (replaces description prop)
extra—Extra content below the input
error{ errors: string[], first: string }Custom error message rendering

FormRule ​

PropertyTypeDescription
requiredbooleanField must have a value
messagestringCustom error message
trigger'blur' | 'change' | ('blur' | 'change')[]When to trigger validation
minnumberMinimum length/value
maxnumberMaximum length/value
lennumberExact length
patternRegExpRegex pattern to match against
typeFormRuleTypeBuilt-in type validator (see below)
validator(value, rule) => boolean | string | PromiseCustom validator function
whitespacebooleanTreat whitespace-only as empty
enumany[]Value must be one of these
notanyValue cannot equal this
samestringMust match another field's value
differentstringMust differ from another field's value
containsstringMust contain this substring
startsWithstringMust start with this string
endsWithstringMust end with this string

Type Validators ​

TypeDescription
stringMust be a string
numberMust be a number
integerMust be a whole number
floatMust be a decimal number
booleanMust be a boolean
arrayMust be an array
emailValid email address
urlValid URL
dateParseable date string
hexValid hex color (#fff, #ff5733)
ipValid IPv4 address
phoneValid phone number format
alphaLetters only (a-z, A-Z)
alphanumericLetters and numbers only