Form Validation
Schema-driven validation built on createForm. Cross-field rules use the validator's own combinators.
See interactive examples below.
1import { createForm } from '@barefootjs/form'2import { z } from 'zod'34const form = createForm({5 schema: z.object({6 name: z.string().min(1, 'Name is required'),7 }),8 defaultValues: { name: '' },9 validateOn: 'blur',10 revalidateOn: 'input',11})1213const name = form.field('name')1415<Input16 value={name.value()}17 onInput={name.handleInput}18 onBlur={name.handleBlur}19/>20<p>{name.error()}</p>#Overview
Validation lives in the schema you pass to createForm. Each field exposes value, error, touched, and the handleInput/handleBlur handlers — wire them to the input. For cross-field rules use the validator's combinators (e.g. Zod's .refine) and target a specific field via path.
#Examples
#Required Field
1import { createForm } from '@barefootjs/form'2import { z } from 'zod'34const form = createForm({5 schema: z.object({6 name: z.string().min(1, 'Name is required'),7 }),8 defaultValues: { name: '' },9 validateOn: 'blur',10 revalidateOn: 'input',11})1213const name = form.field('name')1415<Input16 value={name.value()}17 onInput={name.handleInput}18 onBlur={name.handleBlur}19/>20<p>{name.error()}</p>#Email Format
1const form = createForm({2 schema: z.object({3 email: z4 .string()5 .min(1, 'Email is required')6 .email('Invalid email format'),7 }),8 defaultValues: { email: '' },9 validateOn: 'blur',10 revalidateOn: 'input',11})1213const email = form.field('email')14const isValid = () => email.touched() && email.error() === ''#Cross-Field (Password Confirmation)
1// Use Zod's .refine to compare two fields. The error attaches to2// `confirmPassword` via `path`, so it shows up on `confirm.error()`.3const form = createForm({4 schema: z5 .object({6 password: z.string().min(8, 'Password must be at least 8 characters'),7 confirmPassword: z.string().min(1, 'Please confirm your password'),8 })9 .refine((d) => d.password === d.confirmPassword, {10 message: 'Passwords do not match',11 path: ['confirmPassword'],12 }),13 defaultValues: { password: '', confirmPassword: '' },14 validateOn: 'blur',15 revalidateOn: 'input',16})#Async Availability (Spinner + disabled + aria-busy synced)
1import { createSignal, onCleanup } from '@barefootjs/client'2import { Spinner } from '@/components/ui/spinner'34const TAKEN = new Set(['admin', 'root', 'test', 'guest'])56export function AsyncFieldValidationDemo() {7 const [username, setUsername] = createSignal('')8 const [validating, setValidating] = createSignal(false)9 // 0 neutral · 1 success · 2 warning · 3 error10 const [errorLevel, setErrorLevel] = createSignal(0)11 const [message, setMessage] = createSignal('')1213 // Hue drives `color: hsl(var(--err) 70% 45%)` defined in globals.css.14 const errorHue = () =>15 errorLevel() === 1 ? '140' :16 errorLevel() === 2 ? '40' :17 errorLevel() === 3 ? '0' : '210'1819 let timer = null20 const onInput = (e) => {21 const value = e.target.value22 setUsername(value)23 if (timer) clearTimeout(timer)24 if (!value) { setValidating(false); setErrorLevel(0); setMessage(''); return }2526 setValidating(true)27 setMessage('Checking availability…')28 timer = setTimeout(() => {29 const lower = value.toLowerCase()30 if (TAKEN.has(lower)) { setErrorLevel(3); setMessage(`"${value}" is taken`) }31 else { setErrorLevel(1); setMessage(`"${value}" is available`) }32 setValidating(false)33 }, 600)34 }35 onCleanup(() => timer && clearTimeout(timer))3637 return (38 <form className="space-y-3">39 <label>Username</label>40 <div className="flex items-center gap-2">41 <Input42 value={username()}43 onInput={onInput}44 aria-busy={validating() ? 'true' : 'false'}45 />46 {validating() ? <Spinner className="size-4" /> : null}47 </div>48 <p49 className="async-validation-msg text-sm"50 style={{ '--err': errorHue() }}51 >{message()}</p>52 <Button type="submit" disabled={validating() || errorLevel() === 3}>53 Create account54 </Button>55 </form>56 )57}#Multi-Field Form
1const form = createForm({2 schema: z3 .object({4 name: z.string().min(2, 'Name must be at least 2 characters'),5 email: z.string().email('Invalid email format'),6 password: z.string().min(8, 'Password must be at least 8 characters'),7 confirmPassword: z.string().min(1, 'Please confirm your password'),8 })9 .refine((d) => d.password === d.confirmPassword, {10 message: 'Passwords do not match',11 path: ['confirmPassword'],12 }),13 defaultValues: { name: '', email: '', password: '', confirmPassword: '' },14 validateOn: 'blur',15 revalidateOn: 'input',16 onSubmit: async (data) => {17 await fetch('/api/register', { method: 'POST', body: JSON.stringify(data) })18 },19})2021<form onSubmit={form.handleSubmit}>22 {/* fields ... */}23 <Button type="submit" disabled={form.isSubmitting()}>24 {form.isSubmitting() ? 'Submitting...' : 'Submit'}25 </Button>26</form>