Introduction
Forms in BarefootJS — start with createSignal for the simple cases, reach for @barefootjs/form when things get more involved.
#Simple Form
For a small form, the built-in primitives are usually enough. Pair createSignal with value and onInput for two-way binding — no extra dependencies required.
#Controlled input with createSignal
Current value:
1import { createSignal } from '@barefootjs/client'2import { Input } from '@/components/ui/input'34const [text, setText] = createSignal('')56<Input7 value={text()}8 onInput={(e) => setText(e.target.value)}9 placeholder="Type something..."10/>11<p>Current value: {text()}</p>#When to Reach for @barefootjs/form
Wiring signals by hand stays pleasant for a single input, but starts to add up once a form needs:
- Schema-based validation across multiple fields
- Per-field
touched/dirtystate - Different timing for first validation vs. revalidation (blur vs. input)
- Server-side errors mapped back onto specific fields
- Submission state, reset, and clean default-value tracking
At that point, @barefootjs/form bundles all of it behind a small, declarative API.
#Features
@barefootjs/form is built around Standard Schema, so any compliant validator works out of the box — swap the schema and the rest of the component stays exactly the same.
Validator-agnostic via Standard Schema
Use Zod, Valibot, ArkType, or any other Standard Schema implementation. Migrate between them without touching component code.
Configurable validation timing
validateOn picks when validation first runs ("submit" | "blur" | "input"); revalidateOn picks how it behaves after the first error.
Field controllers
form.field(name) returns a memoized controller exposing value(), error(), touched(), dirty(), and matching handlers.
Server errors and dirty tracking
form.setError() surfaces server-side errors on specific fields; 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#Basic Example
#Profile form with Zod
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}Same component, different validator
Because createForm accepts any Standard Schema validator, swapping the schema definition is the only change needed.
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})#Next Steps
- Validation — error timing, multi-field forms, and custom rules.
- Field Arrays — dynamic add / remove / reorder for repeated fields.