createForm
Schema-driven form management with Standard Schema validation. Replaces manual signal wiring with a declarative API.
1"use client"23import { createForm } from '@barefootjs/form'4import { z } from 'zod'56function ProfileForm() {7 const form = createForm({8 schema: z.object({9 username: z.string()10 .min(1, 'Username is required')11 .max(30, 'Username must be at most 30 characters'),12 }),13 defaultValues: { username: '' },14 onSubmit: async (data) => {15 await fetch('/api/profile', {16 method: 'POST',17 body: JSON.stringify(data),18 })19 },20 })2122 const username = form.field('username')2324 return (25 <form onSubmit={form.handleSubmit}>26 <label>Username</label>27 <input28 value={username.value()}29 onInput={username.handleInput}30 onBlur={username.handleBlur}31 />32 <p>{username.error()}</p>33 <button type="submit" disabled={form.isSubmitting()}>34 {form.isSubmitting() ? 'Submitting...' : 'Submit'}35 </button>36 </form>37 )38}#Overview
createForm provides schema-driven form management using Standard Schema for validation. It works with any schema library that implements the Standard Schema spec — Zod, Valibot, ArkType, and more.
Key features:
- Any Standard Schema validator: Zod, Valibot, ArkType — just swap the schema, everything else stays the same
- Configurable timing:
validateOnandrevalidateOncontrol when validation runs - Field controllers:
form.field("name")returns value, error, touched, dirty, and handlers - Server errors:
form.setError()for server-side validation feedback - Dirty tracking:
form.isDirty()compares current values against defaults
#Installation
1# With Zod2bun add @barefootjs/form zod34# With Valibot5bun add @barefootjs/form valibot67# With ArkType8bun add @barefootjs/form arktype#Examples
#Profile Form
1"use client"23import { createForm } from '@barefootjs/form'4import { z } from 'zod'56function ProfileForm() {7 const form = createForm({8 schema: z.object({9 username: z.string()10 .min(1, 'Username is required')11 .max(30, 'Username must be at most 30 characters'),12 }),13 defaultValues: { username: '' },14 onSubmit: async (data) => {15 await fetch('/api/profile', {16 method: 'POST',17 body: JSON.stringify(data),18 })19 },20 })2122 const username = form.field('username')2324 return (25 <form onSubmit={form.handleSubmit}>26 <label>Username</label>27 <input28 value={username.value()}29 onInput={username.handleInput}30 onBlur={username.handleBlur}31 />32 <p>{username.error()}</p>33 <button type="submit" disabled={form.isSubmitting()}>34 {form.isSubmitting() ? 'Submitting...' : 'Submit'}35 </button>36 </form>37 )38}Basic usage: one field with schema validation. The form validates on submit by default.
Using other validators
createForm accepts any Standard Schema validator. Just swap the schema definition — the rest of the component stays exactly the same.
Valibot
1import { createForm } from '@barefootjs/form'2import * as v from 'valibot'34// Just swap the schema — everything else stays the same5const form = createForm({6 schema: v.object({7 username: v.pipe(8 v.string(),9 v.minLength(1, 'Username is required'),10 v.maxLength(30, 'Username must be at most 30 characters'),11 ),12 }),13 defaultValues: { username: '' },14 onSubmit: async (data) => { /* ... */ },15})ArkType
1import { createForm } from '@barefootjs/form'2import { type } from 'arktype'34const form = createForm({5 schema: type({6 username: '1 <= string <= 30',7 }),8 defaultValues: { username: '' },9 onSubmit: async (data) => { /* ... */ },10})#Login Form
1"use client"23import { createForm } from '@barefootjs/form'4import { z } from 'zod'56function LoginForm() {7 const form = createForm({8 schema: z.object({9 email: z.string().email('Please enter a valid email address'),10 password: z.string().min(8, 'Password must be at least 8 characters'),11 }),12 defaultValues: { email: '', password: '' },13 validateOn: 'blur', // Validate when user leaves field14 revalidateOn: 'input', // Re-validate on every keystroke after first error15 onSubmit: async (data) => {16 await fetch('/api/login', {17 method: 'POST',18 body: JSON.stringify(data),19 })20 },21 })2223 const email = form.field('email')24 const password = form.field('password')2526 return (27 <form onSubmit={form.handleSubmit}>28 <div>29 <label>Email</label>30 <input31 type="email"32 value={email.value()}33 onInput={email.handleInput}34 onBlur={email.handleBlur}35 />36 <p>{email.error()}</p>37 </div>38 <div>39 <label>Password</label>40 <input41 type="password"42 value={password.value()}43 onInput={password.handleInput}44 onBlur={password.handleBlur}45 />46 <p>{password.error()}</p>47 </div>48 <button type="submit" disabled={form.isSubmitting()}>49 {form.isSubmitting() ? 'Signing in...' : 'Sign in'}50 </button>51 </form>52 )53}Multiple fields with validateOn: "blur" and revalidateOn: "input". Errors appear when you leave a field, then clear as you type.
#Notifications (Switch + setValue)
1"use client"23import { createForm } from '@barefootjs/form'4import { Switch } from './ui/switch'5import { z } from 'zod'67function NotificationsForm() {8 const form = createForm({9 schema: z.object({10 marketing: z.boolean(),11 security: z.boolean(),12 }),13 defaultValues: { marketing: false, security: true },14 onSubmit: async (data) => {15 await fetch('/api/notifications', {16 method: 'POST',17 body: JSON.stringify(data),18 })19 },20 })2122 const marketing = form.field('marketing')23 const security = form.field('security')2425 return (26 <form onSubmit={form.handleSubmit}>27 {/* Use setValue() for custom components like Switch */}28 <Switch29 checked={marketing.value()}30 onCheckedChange={(checked) => marketing.setValue(checked)}31 />32 <Switch33 checked={security.value()}34 onCheckedChange={(checked) => security.setValue(checked)}35 />36 <button type="submit" disabled={form.isSubmitting() || !form.isDirty()}>37 Save preferences38 </button>39 {form.isDirty() ? (40 <button type="button" onClick={() => form.reset()}>41 Reset42 </button>43 ) : null}44 </form>45 )46}Use field.setValue() for non-input components. The submit button is disabled until the form is dirty.
#Server Errors (setError)
1"use client"23import { createForm } from '@barefootjs/form'4import { z } from 'zod'56function RegisterForm() {7 const form = createForm({8 schema: z.object({9 email: z.string().email('Please enter a valid email address'),10 username: z.string().min(1, 'Username is required'),11 }),12 defaultValues: { email: '', username: '' },13 validateOn: 'blur',14 revalidateOn: 'input',15 onSubmit: async (data) => {16 const res = await fetch('/api/register', {17 method: 'POST',18 body: JSON.stringify(data),19 })20 if (!res.ok) {21 const errors = await res.json()22 // Set server-side errors on specific fields23 if (errors.email) form.setError('email', errors.email)24 if (errors.username) form.setError('username', errors.username)25 return26 }27 },28 })2930 const email = form.field('email')31 const username = form.field('username')3233 return (34 <form onSubmit={form.handleSubmit}>35 <div>36 <label>Email</label>37 <input38 type="email"39 value={email.value()}40 onInput={email.handleInput}41 onBlur={email.handleBlur}42 />43 <p>{email.error()}</p>44 </div>45 <div>46 <label>Username</label>47 <input48 value={username.value()}49 onInput={username.handleInput}50 onBlur={username.handleBlur}51 />52 <p>{username.error()}</p>53 </div>54 <button type="submit" disabled={form.isSubmitting()}>55 {form.isSubmitting() ? 'Registering...' : 'Register'}56 </button>57 </form>58 )59}Use form.setError() inside onSubmit to display server-side validation errors on specific fields.
#API Reference
createForm(options)
schema— Standard Schema object (Zod, Valibot, ArkType, etc.)defaultValues— Initial field valuesvalidateOn— When to first validate:"input"|"blur"|"submit"(default:"submit")revalidateOn— When to re-validate after first error:"input"|"blur"|"submit"(default:"input")onSubmit— Async callback called with validated data
Form Return
field(name)— Get a field controller (memoized)handleSubmit— Form submit handler (pass to<form onSubmit={...}>)isSubmitting()— Whether submission is in progressisDirty()— Whether any field differs from defaultsisValid()— Whether all fields pass validationerrors()— All current errors keyed by field namereset()— Reset all fields to defaults and clear errorssetError(name, message)— Set an error on a field manually
Field Return
value()— Current field valueerror()— Current validation error messagetouched()— Whether the field has been interacted withdirty()— Whether the value differs from defaultsetValue(value)— Set the field value directlyhandleInput— Input event handler (readse.target.value)handleBlur— Blur event handler (marks touched)