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 source —
FieldDefinition[], JSON array, Zod, Yup, Valibot, or standard JSON Schema / OpenAPI - Headless by default — zero UI dependencies in the core; bring your own components
- Reactive conditions —
visible,disabledaccept a boolean, function, or string expression - Dynamic options — sync and async
optionsfunctions with dependency tracking (optionsDeps) - Dynamic array fields —
type: 'array'withuseFieldArraycomposable (append / remove / move / swap) - Multi-step wizard —
useMultiStepFormwith per-step validation andMultiStepFormRenderer - Validation — sync + async validators,
validateMode: 'first' | 'all',validateOn: 'eager' - Cross-field —
sameAsvalidator; validators receive all current values as second argument - Transform & parse —
transformruns on everysetField;parseruns at submit time - File upload —
type: 'file'withfileType,fileSize,fileCountvalidators; drag-and-drop UI - Custom components —
field.component+ per-app and per-subtree component registry - Input masking — phone (RU/EU), date, IBAN, INN, custom
#/Apatterns; no external deps - Schema composition —
mergeSchemas,omitFields,pickFields,extendField - Discriminated schemas —
discriminatedFieldsbuildsvisiblewiring for a field set that switches entirely by a discriminator value, with nativez.discriminatedUnion/v.variantmapping - TypeScript inference —
InferValues<T>maps schema literals to typed values - Persisted forms —
persist: 'local' | 'session'with SSR-safe storage - Server-side validation errors —
applyServerErrorsmaps Laravel/DRF/flat/custom formats ontoerrors - Debug mode —
debug: truelogs state changes;useFormDebugreturns a reactive snapshot - Tailwind UI theme —
vue-form-schema/ui/tailwindsubentry with utility-class components - shadcn / PrimeVue / Naive UI themes — drop-in renderers for popular component libraries
- Accessibility —
aria-required,aria-invalid,aria-describedby,fieldset/legendfor 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-schemaauto-imports composables, validators and schema adapters - Vue DevTools —
vue-form-schema/devtoolsadds a live forms inspector + timeline, zero cost when not installed
Installation
bash
npm install @macrulez/vue-form-schemaOptional peer dependencies:
bash
npm install zod # Zod adapter
npm install yup # Yup adapter
npm install valibot # Valibot adapterUsing 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-schemats
// 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>