.md

Quick start

A typed schema, validated inputs, a submit handler. The minimum viable form in five minutes.

Time
~5 minutes
You'll learn
useForm + register + handleSubmit

Try the form below: clear the password and submit to watch focus pull to the broken field; submit with valid values to see the alert fire. Every behavior on screen comes from the Zod schema in code, which you'll see in the Build a form section next.

Build a form

Hand useForm a Zod schema and the reactive form comes back ready. This page reaches for three properties on the returned form: register for the input binding, handleSubmit for the submit gate, and fields for per-field error reads.

import { useForm } from 'attaform'
import { z } from 'zod'

const schema = z.object({
  email: z.email('Enter a valid email'),
  password: z.string().min(8, 'At least 8 characters'),
})

const form = useForm({ schema })

const onSubmit = form.handleSubmit(async (values) => {
  await fetch('/api/sign-in', { method: 'POST', body: JSON.stringify(values) })
})

Bind inputs to schema paths with v-register:

<template>
  <form @submit="onSubmit">
    <label>
      Email
      <input v-register="form.register('email')" autocomplete="email" />
      <em v-if="form.fields.email.showErrors">{{ form.fields.email.firstError?.message }}</em>
    </label>
    <label>
      Password
      <input v-register="form.register('password')" type="password" autocomplete="off" />
      <em v-if="form.fields.password.showErrors">{{ form.fields.password.firstError?.message }}</em>
    </label>
    <button :disabled="form.meta.submitting" type="submit">Sign in</button>
  </form>
</template>

form.register('email') returns what the v-register directive binds to. The directive handles the value read, the write, the coercion, and focus on invalid submit. Errors render via form.fields.<path>.firstError?.message, gated by form.fields.<path>.showErrors so the form doesn't yell on first paint.

What's next