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

createForm

Schema-driven form management with Standard Schema validation. Replaces manual signal wiring with a declarative API.

This is your public display name.

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: validateOn and revalidateOn control 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

This is your public display name.

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)

Email Notifications

Configure which emails you want to receive.

Receive emails about new products and features.

Receive emails about your account security.

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)

Try "taken@example.com" or username "admin" to see server errors.

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 values
  • validateOn — 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 progress
  • isDirty() — Whether any field differs from defaults
  • isValid() — Whether all fields pass validation
  • errors() — All current errors keyed by field name
  • reset() — Reset all fields to defaults and clear errors
  • setError(name, message) — Set an error on a field manually

Field Return

  • value() — Current field value
  • error() — Current validation error message
  • touched() — Whether the field has been interacted with
  • dirty() — Whether the value differs from default
  • setValue(value) — Set the field value directly
  • handleInput — Input event handler (reads e.target.value)
  • handleBlur — Blur event handler (marks touched)