Your rules, in one place
Which fields exist, when they show, what makes them valid. The whole behaviour of a form lives in one readable definition instead of being scattered through UI code, so changes stay cheap and safe for years.
An open source Vue form engine that keeps your business rules in one readable place, while the design stays fully yours. Built on top of vee-validate.
The forms that matter are never three fields and a button. They repeat ("add another applicant"), they branch (person or company), and they carry rules ("if business, require a VAT number"). Hand-built, those rules end up scattered through the UI code until nobody can safely answer "what happens if I change this field?"
Vue Dynamic Form separates how a form behaves from how it looks. The behaviour lives in one plain, readable definition, built on a model inspired by XSD (the schema language behind government, insurance, customs, and tax systems), so the hard cases were battle-tested long before this library existed. The look is built entirely from your own components on top of vee-validate, the most widely used Vue validation library. The whole engine is MIT licensed: no per-developer fees, no production licences, no paywalled features.
Curious how it stacks up against SurveyJS, FormKit, Form.io and the JSON Schema tools? Read Why Vue Dynamic Form.
A concrete demo for a SaaS team collecting everything needed to onboard a new client. It highlights reusable templates, grouped fields, repeatable contacts, a repeatable choice of systems shown and submitted in the order they were added, mutually exclusive launch paths, and computed fields that become required only when they matter.
IsDirty: false
Touched: false
Valid: true
// form values:
{
"company": {},
"projectContacts": [
{}
]
}
Most form logic comes down to "if this field has a certain value, show/hide/require/change that other field", and it usually ends up scattered across v-ifs, computed properties, and watch() calls. Here it lives with the field itself: the form definition decides when something happens, and your template decides what that looks like in HTML.
const fields: Metadata[] = [
{ name: 'accountType', type: 'select', ... },
{
name: 'vatNumber',
type: 'text',
fieldOptions: { label: 'VAT Number' },
computedProps: [
(field) => { field.disabled = accountType.value !== 'business' }
]
}
]<template #text-input="{ fieldMetadata, fieldContext: { value, handleChange } }">
<input
type="text"
:disabled="fieldMetadata.disabled"
:value="value.value"
@input="handleChange"
>
</template>Vue tracks whatever the function reads and re-runs it when that changes, no watch() needed. And disabled isn't a built-in: it's a property you declare in your template's metadata contract, so the vocabulary of behaviours is entirely yours. Components stay clean and reusable; the form definition is the single source of truth for behaviour.