Form
Form wrapper with layout, validation, and state management for form controls.
<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
<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
<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.
<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.
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.
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.
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.
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
<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
<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
<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.
<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.
<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.
<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.
<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.
<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.
<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.
<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.
<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.
<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.
<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.
<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
| Prop | Type | Default | Description |
|---|---|---|---|
model | Record<string, any> | — | Form data object |
rules | FormRules | — | Validation rules keyed by field name |
layout | 'vertical' | 'horizontal' | 'inline' | 'vertical' | Form layout |
labelWidth | string | number | — | Label width for horizontal layout |
labelAlign | 'left' | 'right' | 'top' | 'right' | Label text alignment in horizontal layout |
labelSuffix | string | '' | Suffix appended to all labels (e.g. :) |
size | 'sm' | 'default' | 'lg' | 'default' | Form size |
disabled | boolean | false | Disable entire form |
loading | boolean | false | Loading state — disables pointer events |
showRequiredMark | boolean | true | Show red asterisk on required fields |
requiredMarkPosition | 'left' | 'right' | 'left' | Position of the required asterisk |
validateOnChange | boolean | true | Auto-validate on model change |
validateOnBlur | boolean | true | Auto-validate on field blur |
hideErrorMessage | boolean | false | Hide all error messages |
scrollToError | boolean | false | Scroll to first error after validation |
statusIcon | boolean | false | Show validation status icons next to inputs |
showAllErrors | boolean | false | Show all validation errors instead of first only |
FormItem
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | '' | Label text |
field | string | '' | Field name (must match key in model and rules) |
rules | FormRule | FormRule[] | — | Field-level rules (merged with form-level) |
required | boolean | — | Override required state (auto-detected from rules) |
labelWidth | string | number | — | Override form-level label width |
showMessage | boolean | true | Show error message for this field |
help | string | '' | Help text below the field |
description | string | '' | Tooltip text shown via info icon next to label |
dependencies | string[] | — | Re-validate this field when listed fields change |
Events
| Event | Payload | Description |
|---|---|---|
validate | (field, valid, message) | Fired after a field is validated |
submit | Event | Native form submit (prevented by default) |
Exposed Methods
| Method | Signature | Description |
|---|---|---|
validate | () => Promise<{ valid, errors }> | Validate all fields |
validateFields | (fields: string[]) => Promise<{ valid, errors }> | Validate specific fields |
resetFields | () => void | Reset validation state |
clearValidation | (fields?: string[]) => void | Clear error messages |
isDirty | ComputedRef<boolean> | Whether any field has been modified |
dirtyFields | ComputedRef<string[]> | List of modified field names |
scrollToFirstError | (field: string) => void | Scroll to a specific field |
Slots
FormItem
| Slot | Props | Description |
|---|---|---|
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
| Property | Type | Description |
|---|---|---|
required | boolean | Field must have a value |
message | string | Custom error message |
trigger | 'blur' | 'change' | ('blur' | 'change')[] | When to trigger validation |
min | number | Minimum length/value |
max | number | Maximum length/value |
len | number | Exact length |
pattern | RegExp | Regex pattern to match against |
type | FormRuleType | Built-in type validator (see below) |
validator | (value, rule) => boolean | string | Promise | Custom validator function |
whitespace | boolean | Treat whitespace-only as empty |
enum | any[] | Value must be one of these |
not | any | Value cannot equal this |
same | string | Must match another field's value |
different | string | Must differ from another field's value |
contains | string | Must contain this substring |
startsWith | string | Must start with this string |
endsWith | string | Must end with this string |
Type Validators
| Type | Description |
|---|---|
string | Must be a string |
number | Must be a number |
integer | Must be a whole number |
float | Must be a decimal number |
boolean | Must be a boolean |
array | Must be an array |
email | Valid email address |
url | Valid URL |
date | Parseable date string |
hex | Valid hex color (#fff, #ff5733) |
ip | Valid IPv4 address |
phone | Valid phone number format |
alpha | Letters only (a-z, A-Z) |
alphanumeric | Letters and numbers only |