Get Started
Introduction
Components
Accordion
Badge
Button
Card
Checkbox
Command
Dialog
Dropdown Menu
Input
Select
Switch
Table
Tabs
Toast
Tooltip
Forms
Introduction
Validation
Field Arrays

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>