Quick start

Get a working Attaform form on screen in under five minutes. The mainline path is Nuxt — bare-Vue + Vite is one section down.

1. Install

$ pnpm add attaform zod

zod is a peer dependency. Requires Vue 3 and Zod 4 — for Zod v3, swap the import for attaform/zod-v3 (same surface, separate adapter).

2. Wire it up

Nuxt 3 / 4

Add the module to nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['attaform/nuxt'],
})

That's everything. The module installs the plugin, registers the v-register directive, and auto-imports useForm so you can call it without an explicit import.

Bare Vue + Vite

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { attaform } from 'attaform/vite'

export default defineConfig({
  plugins: [vue(), attaform()],
})
// main.ts
import { createApp } from 'vue'
import { createAttaform } from 'attaform'
import App from './App.vue'

createApp(App).use(createAttaform()).mount('#app')

The Vite plugin is required for SSR-correct v-register bindings. For other bundlers, see attaform/transforms.

3. Your first form

<script setup lang="ts">
  import { z } from 'zod'
  import { useForm } from 'attaform/zod' // or auto-imported under Nuxt

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

  const form = useForm({ schema, key: 'signup' })

  const onSubmit = form.handleSubmit(async (values) => {
    // `values` is fully typed from the schema — no `as`, no manual narrowing.
    await api.signup(values)
  })
</script>

<template>
  <form @submit.prevent="onSubmit">
    <label>
      Email
      <input v-register="form.register('email')" type="email" />
      <small>{{ form.errors.email?.[0]?.message }}</small>
    </label>

    <label>
      Password
      <input v-register="form.register('password')" type="password" />
      <small>{{ form.errors.password?.[0]?.message }}</small>
    </label>

    <button type="submit" :disabled="!form.meta.isValid || form.meta.isSubmitting">
      {{ form.meta.isSubmitting ? 'Creating account…' : 'Create account' }}
    </button>
  </form>
</template>

Three things this demonstrates:

  • v-register binds a native input directly to a path on the form. No two-way wiring, no manual v-model plumbing.
  • form.errors is a reactive proxy. Refinement errors surface as the user types; required-field "no value supplied" errors fire on submit.
  • form.meta.isValid and form.meta.isSubmitting gate the submit button — submit auto-runs validation first, so the callback receives strictly-typed values.

Open the live playground to edit this exact example without leaving the browser.

4. Where to next

If you want to…Read
See every option useForm acceptsattaform/zod
Understand form.values / form.errors / metaThe useForm return value
Persist drafts across reloadsPersistence
Validate on blur instead of changeField-level validation
Handle server-side validation errorsServer errors
Add undo / redoUndo / redo
Build dynamic field arrays (append/remove/…)Dynamic field arrays
Hit a problemTroubleshooting