CForm
CForm is a wrapper component for forms. It coordinates validation across all child fields (CTextField, CInput, and any other CInput-based components), prevents native browser submission, and supports async validators.
Basic example
A login form with email and password validation triggered on blur.
Sign In
Welcome back
Show code
vue
<template>
<CForm ref="formRef">
<template #default="{ validate, reset }">
<CTextField
v-model="form.email"
label="Email"
type="email"
:rules="emailRules"
validate-on="blur"
>
<template #prepend><CIcon name="fas:envelope" :size="16" source="fa" /></template>
</CTextField>
<CTextField
v-model="form.password"
:type="showPwd ? 'text' : 'password'"
label="Password"
:rules="passwordRules"
validate-on="blur"
>
<template #prepend><CIcon name="fas:lock" :size="16" source="fa" /></template>
<template #append>
<CIcon
:name="showPwd ? 'fas:eye-slash' : 'fas:eye'"
:size="16"
source="fa"
style="cursor:pointer"
@click="showPwd = !showPwd"
/>
</template>
</CTextField>
<CBtn @click="() => handleSubmit(validate)">Sign in</CBtn>
<CBtn variant="text" @click="handleReset">Reset</CBtn>
</template>
</CForm>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formRef = ref()
const showPwd = ref(false)
const form = ref({ email: '', password: '' })
const emailRules = [
(v: string) => ({ valid: !!v, message: 'Email is required' }),
(v: string) => ({ valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'Invalid email' }),
]
const passwordRules = [
(v: string) => ({ valid: !!v, message: 'Password is required' }),
(v: string) => ({ valid: v.length >= 6, message: 'Minimum 6 characters' }),
]
async function handleSubmit(validate: () => Promise<boolean>) {
if (await validate()) console.log('submit', form.value)
}
function handleReset() {
form.value = { email: '', password: '' }
}
</script>Profile form
A multi-field profile editor with a two-column layout, a readonly field, and an optional phone/website.
Edit Profile
Update your information
Show code
vue
<template>
<CForm ref="formRef">
<template #default="{ validate, reset }">
<div class="form-grid">
<CTextField v-model="form.firstName" label="First name" :rules="requiredRule" validate-on="blur" preset="input.indigo" />
<CTextField v-model="form.lastName" label="Last name" :rules="requiredRule" validate-on="blur" preset="input.indigo" />
<CTextField v-model="form.email" label="Email" type="email" :rules="emailRules" validate-on="blur" preset="input.indigo">
<template #prepend><CIcon name="fas:envelope" :size="14" source="fa" /></template>
</CTextField>
<CTextField v-model="form.phone" label="Phone" type="tel" :rules="phoneRules" validate-on="blur" preset="input.indigo">
<template #prepend><CIcon name="fas:phone" :size="14" source="fa" /></template>
</CTextField>
<!-- readonly field — username cannot be changed -->
<CTextField v-model="form.username" label="Username" readonly preset="input.indigo">
<template #prepend><CIcon name="fas:at" :size="14" source="fa" /></template>
<template #details>
<span style="opacity:.6; font-size:12px">Username cannot be changed</span>
</template>
</CTextField>
<div>
<CBtn class="bg-indigo" style="color:#fff" @click="() => handleSave(validate)">Save</CBtn>
<CBtn variant="text" @click="handleReset">Cancel</CBtn>
</div>
</div>
</template>
</CForm>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formRef = ref()
const form = ref({ firstName: 'Alex', lastName: 'Johnson', email: 'alex@example.com', phone: '', username: 'alexjohnson' })
const original = { ...form.value }
const requiredRule = [(v: string) => ({ valid: !!v?.trim(), message: 'Required' })]
const emailRules = [
(v: string) => ({ valid: !!v, message: 'Required' }),
(v: string) => ({ valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'Invalid email' }),
]
const phoneRules = [
(v: string) => ({ valid: !v || /^\+?[\d\s\-()]{7,}$/.test(v), message: 'Invalid phone' }),
]
async function handleSave(validate: () => Promise<boolean>) {
if (await validate()) console.log('saved', form.value)
}
function handleReset() {
form.value = { ...original }
}
</script>Multi-step form
A multi-step airline booking form with per-step validation, async submission, and a success screen.
VueAir
1
Flight2
Passenger3
PaymentShow code
vue
<template>
<CForm ref="formRef">
<template #default="{ validate, reset }">
<!-- Step 1: Flight details -->
<div v-if="currentStep === 0">
<CTextField v-model="flight.from" label="From" :rules="requiredRule" validate-on="blur" preset="input.blue" />
<CTextField v-model="flight.to" label="To" :rules="requiredRule" validate-on="blur" preset="input.blue" />
<CTextField v-model="flight.departure" label="Departure date" placeholder="DD.MM.YYYY" :rules="departureDateRule" validate-on="blur" preset="input.blue" />
<CTextField v-model="flight.passengers" label="Passengers" type="number" :rules="passengersRule" validate-on="blur" preset="input.blue" />
</div>
<!-- Step 2: Passenger info -->
<div v-if="currentStep === 1">
<CTextField v-model="passenger.firstName" label="First name" :rules="requiredRule" validate-on="blur" preset="input.teal" />
<CTextField v-model="passenger.lastName" label="Last name" :rules="requiredRule" validate-on="blur" preset="input.teal" />
<CTextField v-model="passenger.passport" label="Passport" :rules="passportRule" validate-on="blur" preset="input.teal" />
<CTextField v-model="passenger.email" label="Email" type="email" :rules="emailRules" validate-on="blur" preset="input.teal" />
</div>
<!-- Step 3: Payment -->
<div v-if="currentStep === 2">
<CTextField v-model="payment.card" label="Card number" :rules="cardRule" validate-on="blur" preset="input.deepPurple" />
<CTextField v-model="payment.expiry" label="Expiry (MM/YY)" :rules="expiryRule" validate-on="blur" preset="input.deepPurple" />
<CTextField v-model="payment.cvv" label="CVV" type="password" :rules="cvvRule" validate-on="blur" preset="input.deepPurple" />
</div>
<CBtn v-if="currentStep > 0" variant="text" @click="currentStep--">Back</CBtn>
<CBtn @click="() => handleNext(validate)">
{{ currentStep === 2 ? 'Pay & Confirm' : 'Continue' }}
</CBtn>
</template>
</CForm>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const currentStep = ref(0)
const flight = ref({ from: '', to: '', departure: '', passengers: '1' })
const passenger = ref({ firstName: '', lastName: '', passport: '', email: '' })
const payment = ref({ card: '', expiry: '', cvv: '' })
const requiredRule = [(v: string) => ({ valid: !!v?.trim(), message: 'Required' })]
const passengersRule = [
(v: string) => ({ valid: Number(v) >= 1 && Number(v) <= 9, message: '1–9 passengers' }),
]
const passportRule = [
(v: string) => ({ valid: /^[A-Z0-9]{6,9}$/i.test(v), message: 'Invalid passport number' }),
]
const emailRules = [
(v: string) => ({ valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'Invalid email' }),
]
const cardRule = [
(v: string) => ({ valid: v.replace(/\s/g, '').length === 16, message: '16-digit card number' }),
]
const expiryRule = [(v: string) => ({ valid: /^\d{2}\/\d{2}$/.test(v), message: 'MM/YY format' })]
const cvvRule = [(v: string) => ({ valid: /^\d{3,4}$/.test(v), message: '3–4 digits' })]
async function handleNext(validate: () => Promise<boolean>) {
if (await validate()) currentStep.value++
}
</script>Async validation
Rules can return a Promise. CForm.validate() runs all fields in parallel via Promise.all.
Create Account
Fill in your details below
Show code
vue
<template>
<CForm>
<template #default="{ validate, reset }">
<CTextField
v-model="form.username"
label="Username"
:rules="usernameRules"
validate-on="blur"
>
<template #details="{ errorMessage, hasError, validating }">
<span v-if="validating" style="color: var(--c-app-primary-color)">Checking…</span>
<span v-else-if="hasError" style="color: var(--c-app-error-color)">{{ errorMessage }}</span>
<span v-else style="opacity:.6">Letters and numbers only</span>
</template>
</CTextField>
<CTextField v-model="form.email" label="Email" type="email" :rules="emailRules" validate-on="blur" />
<CBtn @click="() => handleSubmit(validate)">Register</CBtn>
</template>
</CForm>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const form = ref({ username: '', email: '' })
const taken = ['admin', 'root', 'vueland']
const usernameRules = [
(v: string) => ({ valid: /^[a-zA-Z0-9_]{3,}$/.test(v), message: 'Min 3 chars, letters/numbers/_' }),
async (v: string) => {
await new Promise(r => setTimeout(r, 600))
return { valid: !taken.includes(v.toLowerCase()), message: `"${v}" is already taken` }
},
]
const emailRules = [
(v: string) => ({ valid: !!v, message: 'Required' }),
(v: string) => ({ valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'Invalid email' }),
]
async function handleSubmit(validate: () => Promise<boolean>) {
if (await validate()) console.log('submit', form.value)
}
</script>API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | aria-label for the <form> element. When present, the browser automatically assigns role="form" |
Slots
| Slot | Props | Description |
|---|---|---|
default | { validate, reset } | Form content with validate and reset functions |
default slot props
| Prop | Type | Description |
|---|---|---|
validate | () => Promise<boolean> | Run validation on all registered fields. Returns true if every field is valid |
reset | () => void | Reset validation state on all registered fields |
Events
| Event | Arguments | Description |
|---|---|---|
submit | Event | Emitted when the form is submitted. The native submit is prevented automatically |
Expose
| Method | Signature | Description |
|---|---|---|
validate | () => Promise<boolean> | Run validation on all registered fields |
reset | () => void | Reset validation state on all registered fields |
vue
<template>
<CForm ref="formRef">
<template #default><!-- fields --></template>
</CForm>
<CBtn @click="formRef?.validate()">Validate externally</CBtn>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formRef = ref()
</script>How it works
- Child components (
CTextField,CInput) register theirvalidatefunction with the nearestCFormviaprovide/injecton mount. CForm.validate()runs all registered functions viaPromise.all(in parallel).- When a child unmounts, its function is automatically removed.
CForm
├─ CTextField → registers validate on mount
├─ CTextField → registers validate on mount
└─ CInput → registers validate on mount
form.validate()
→ Promise.all([field1.validate(), field2.validate(), field3.validate()])
→ true / falseARIA and accessibility
| Attribute | Value | Description |
|---|---|---|
novalidate | present | Disables native browser validation |
aria-label | value of label prop | Identifies the form to screen readers |
role="form" | auto | Assigned by the browser when aria-label is present |
TIP
Pass a meaningful label when there are multiple forms on the page so screen reader users can distinguish between them.
