Skip to content

vue-form-schema

Reactive forms from a declarative schema (JSON, Zod, Yup, or Valibot) for Vue 3. A headless, SSR-compatible alternative to VeeValidate / FormKit for forms that are generated dynamically or driven from the server.

Features

  • Any schema sourceFieldDefinition[], JSON array, Zod, Yup, Valibot, or standard JSON Schema / OpenAPI
  • Headless by default — zero UI dependencies in the core; bring your own components
  • Reactive conditionsvisible, disabled accept a boolean, function, or string expression
  • Dynamic options — sync and async options functions with dependency tracking (optionsDeps)
  • Dynamic array fieldstype: 'array' with useFieldArray composable (append / remove / move / swap)
  • Multi-step wizarduseMultiStepForm with per-step validation and MultiStepFormRenderer
  • Validation — sync + async validators, validateMode: 'first' | 'all', validateOn: 'eager'
  • Cross-fieldsameAs validator; validators receive all current values as second argument
  • Transform & parsetransform runs on every setField; parse runs at submit time
  • File uploadtype: 'file' with fileType, fileSize, fileCount validators; drag-and-drop UI
  • Custom componentsfield.component + per-app and per-subtree component registry
  • Input masking — phone (RU/EU), date, IBAN, INN, custom #/A patterns; no external deps
  • Schema compositionmergeSchemas, omitFields, pickFields, extendField
  • Discriminated schemasdiscriminatedFields builds visible wiring for a field set that switches entirely by a discriminator value, with native z.discriminatedUnion / v.variant mapping
  • TypeScript inferenceInferValues<T> maps schema literals to typed values
  • Persisted formspersist: 'local' | 'session' with SSR-safe storage
  • Server-side validation errorsapplyServerErrors maps Laravel/DRF/flat/custom formats onto errors
  • Debug modedebug: true logs state changes; useFormDebug returns a reactive snapshot
  • Tailwind UI themevue-form-schema/ui/tailwind subentry with utility-class components
  • shadcn / PrimeVue / Naive UI themes — drop-in renderers for popular component libraries
  • Accessibilityaria-required, aria-invalid, aria-describedby, fieldset/legend for radio
  • SSR-safe — no direct browser APIs in the core
  • Tree-shakeable — Zod/Yup/Valibot adapters and UI are separate entry points
  • Nuxt module@macrulez/nuxt-vue-form-schema auto-imports composables, validators and schema adapters
  • Vue DevToolsvue-form-schema/devtools adds a live forms inspector + timeline, zero cost when not installed

Installation

bash
npm install @macrulez/vue-form-schema

Optional peer dependencies:

bash
npm install zod       # Zod adapter
npm install yup       # Yup adapter
npm install valibot   # Valibot adapter

Using Nuxt?

@macrulez/nuxt-vue-form-schema auto-imports useForm, useFieldArray, the built-in validators, schema adapters and more — no manual import needed:

bash
npm install @macrulez/nuxt-vue-form-schema
ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@macrulez/nuxt-vue-form-schema'],
})

See the Nuxt Module page for options.

Quick start

vue
<script setup lang="ts">
import { useForm } from '@macrulez/vue-form-schema'
import type { FieldDefinition } from '@macrulez/vue-form-schema'

const schema: FieldDefinition[] = [
  { type: 'text', name: 'name', label: 'Full name', required: true },
  { type: 'email', name: 'email', label: 'Email', required: true },
  {
    type: 'select',
    name: 'role',
    label: 'Role',
    options: [
      { label: 'Admin', value: 'admin' },
      { label: 'User', value: 'user' },
    ],
  },
]

const { values, errors, touched, isValid, isSubmitting, submit, setField } = useForm({
  schema,
  validateOn: 'blur',
  onSubmit: async (data) => {
    await fetch('/api/users', { method: 'POST', body: JSON.stringify(data) })
  },
})
</script>

<template>
  <form @submit.prevent="submit">
    <div v-for="field in schema" :key="field.name">
      <label>{{ field.label }}</label>
      <input
        :type="field.type"
        :value="values[field.name]"
        @input="setField(field.name, ($event.target as HTMLInputElement).value)"
        @blur="touched[field.name] = true"
      />
      <span v-if="touched[field.name] && errors[field.name]">
        {{ errors[field.name][0] }}
      </span>
    </div>
    <button type="submit" :disabled="!isValid || isSubmitting">Submit</button>
  </form>
</template>

Or use FormRenderer for zero-markup rendering:

vue
<script setup lang="ts">
import { useForm } from '@macrulez/vue-form-schema'
import { FormRenderer } from '@macrulez/vue-form-schema/ui'

const form = useForm({ schema, onSubmit })
</script>

<template>
  <FormRenderer :form="form" submit-label="Save" />
</template>